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,20 @@
1
+ require_relative '../lib/rubykon'
2
+ require_relative 'support/benchmark-ips'
3
+ require_relative 'support/playout_help'
4
+
5
+ Benchmark.ips do |benchmark|
6
+ game_9 = playout_for(9).game_state.game
7
+ game_13 = playout_for(13).game_state.game
8
+ game_19 = playout_for(19).game_state.game
9
+ scorer = Rubykon::GameScorer.new
10
+
11
+ benchmark.report '9x9 scoring' do
12
+ scorer.score game_9
13
+ end
14
+ benchmark.report '13x13 scoring' do
15
+ scorer.score game_13
16
+ end
17
+ benchmark.report '19x19 scoring' do
18
+ scorer.score game_19
19
+ end
20
+ end
@@ -0,0 +1,60 @@
1
+ require_relative '../lib/rubykon'
2
+ require_relative 'support/playout_help'
3
+ require_relative 'support/benchmark-ips'
4
+
5
+ class Rubykon::GameScorer
6
+ public :score_empty_cutting_point
7
+ public :find_candidate_color
8
+ public :only_one_color_adjacent?
9
+ end
10
+
11
+ Benchmark.ips do |benchmark|
12
+
13
+ board = Rubykon::Board.from <<-BOARD
14
+ OOOOO-O-OOOOOOXXXXX
15
+ O-OOOOOO-O-OOOX-X-X
16
+ OO-OOXOXOOOO-OOXXXX
17
+ -OOOXXXXXXOOOOOX-X-
18
+ OOOOOXOXXXOOOOOOXXX
19
+ OXXOOOOOXOOOO-OOOOX
20
+ X-XOOOXXXOOOOOO-OOO
21
+ XXXOXXXOXXOOOOOOO-O
22
+ XX-XXXOOOOO-OOO-OOO
23
+ X-XXXXOXXO-OOOOOOOO
24
+ -XXXXXXXXXO-OOO-OOO
25
+ XXXX-X-XXXXOOXOO-O-
26
+ XX-XX-XX-XOOXXOOOOO
27
+ -XXXXX-XXXXOX-XOOOO
28
+ XXX-XXXXXXXXXXXXOXO
29
+ XXXX-XXXXX-X-XXXOXO
30
+ -XXXX-X-XXXXXXXXXXO
31
+ X-XX-XXXX-X-XX-XOOO
32
+ -XXXXX-XXXXXXXXXO-O
33
+ BOARD
34
+ scorer = Rubykon::GameScorer.new
35
+ identifier = board.identifier_for(3, 3)
36
+
37
+
38
+
39
+ benchmark.report 'score_empty_cp' do
40
+ game_score = {Rubykon::Board::BLACK => 0,
41
+ Rubykon::Board::WHITE => Rubykon::Game::DEFAULT_KOMI}
42
+ scorer.score_empty_cutting_point(identifier, board, game_score)
43
+ end
44
+
45
+ benchmark.report 'Board#neighbour_colors_of' do
46
+ board.neighbour_colors_of(identifier)
47
+ end
48
+
49
+ neighbour_colors = board.neighbour_colors_of(identifier)
50
+
51
+ benchmark.report 'find_candidate_color' do
52
+ scorer.find_candidate_color(neighbour_colors)
53
+ end
54
+
55
+ candidate_color = scorer.find_candidate_color(neighbour_colors)
56
+
57
+ benchmark.report 'only_one_c_adj?' do
58
+ scorer.only_one_color_adjacent?(neighbour_colors, candidate_color)
59
+ end
60
+ end
@@ -0,0 +1,11 @@
1
+ def truffle? # truffle can't do gem install
2
+ defined?(RUBY_DESCRIPTION) && RUBY_DESCRIPTION.match(/graal/)
3
+ end
4
+
5
+ require 'benchmark/ips'
6
+
7
+ # only loaded for truffle normally, as it has little to no effect on other
8
+ # implementations I tested and only results in them running WAY longer.
9
+ if truffle?
10
+ require_relative 'benchmark-ips_shim'
11
+ end
@@ -0,0 +1,143 @@
1
+ # Shim modifying benchmark-ips to work better with truffle,
2
+ # written by Chris Seaton and taken from:
3
+ # https://gist.github.com/chrisseaton/1c4cb91f3c95ddcf2d1e
4
+ # Note that this has very little effect on the performance of CRuby/JRuby.
5
+ # I tried it out and results were ~2 to 4 % better which is well within
6
+ # the tolerance. On a pre 0.3 version this moved truffle fromm 33 ips of
7
+ # 19x19 gameplay up to 169.
8
+
9
+ # This file modifies benchmark-ips to better accommodate the optimisation
10
+ # characteristics of sophisticated implementations of Ruby that have a very
11
+ # large difference between cold and warmed up performance, and that apply
12
+ # optimisations such as value profiling or other speculation on runtime values.
13
+ # Recommended to be used with a large (60s) warmup and (30s) measure time. This
14
+ # has been modified to be the default. Note that on top of that, it now runs
15
+ # warmup five times, so generating the report will be a lot slower than
16
+ # before.
17
+
18
+ # Code is modified from benchmark-ips
19
+
20
+ # Copyright (c) 2015 Evan Phoenix
21
+ #
22
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
23
+ # of this software and associated documentation files (the 'Software'), to deal
24
+ # in the Software without restriction, including without limitation the rights
25
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26
+ # copies of the Software, and to permit persons to whom the Software is
27
+ # furnished to do so, subject to the following conditions:
28
+ #
29
+ # The above copyright notice and this permission notice shall be included in
30
+ # all copies or substantial portions of the Software.
31
+ #
32
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
+ # SOFTWARE.
39
+
40
+ module Benchmark
41
+ module IPS
42
+ class Job
43
+
44
+ def run_warmup
45
+ @list.each do |item|
46
+ @suite.warming item.label, @warmup if @suite
47
+
48
+ unless @quiet
49
+ $stdout.print item.label_rjust
50
+ end
51
+
52
+ Timing.clean_env
53
+
54
+ # Modification - run with different iteration parameters to defeat
55
+ # value profiling and other speculation on runtime values.
56
+
57
+ item.call_times 1
58
+ item.call_times 2
59
+ item.call_times 3
60
+
61
+ # Modification - actual time to warm up - not measured
62
+
63
+ target = Time.now + @warmup
64
+ while Time.now < target
65
+ item.call_times 1
66
+ end
67
+
68
+ before = Time.now
69
+ target = Time.now + @warmup
70
+
71
+ warmup_iter = 0
72
+
73
+ while Time.now < target
74
+ item.call_times(1)
75
+ warmup_iter += 1
76
+ end
77
+
78
+ after = Time.now
79
+
80
+ warmup_time_us = time_us before, after
81
+
82
+ @timing[item] = cycles_per_100ms warmup_time_us, warmup_iter
83
+
84
+ # Modification - warm up again with this new iteration value that we
85
+ # haven't run before.
86
+
87
+ cycles = @timing[item]
88
+ target = Time.now + @warmup
89
+
90
+ while Time.now < target
91
+ item.call_times cycles
92
+ end
93
+
94
+ # Modification repeat the scaling again
95
+
96
+ before = Time.now
97
+ target = Time.now + @warmup
98
+
99
+ warmup_iter = 0
100
+
101
+ while Time.now < target
102
+ item.call_times cycles
103
+ warmup_iter += cycles
104
+ end
105
+
106
+ after = Time.now
107
+
108
+ warmup_time_us = time_us before, after
109
+
110
+ @timing[item] = cycles_per_100ms warmup_time_us, warmup_iter
111
+
112
+ case Benchmark::IPS.options[:format]
113
+ when :human
114
+ $stdout.printf "%s i/100ms\n", Helpers.scale(@timing[item]) unless @quiet
115
+ else
116
+ $stdout.printf "%10d i/100ms\n", @timing[item] unless @quiet
117
+ end
118
+
119
+ @suite.warmup_stats warmup_time_us, @timing[item] if @suite
120
+
121
+ # Modification - warm up again with this new iteration value that we
122
+ # haven't run before.
123
+
124
+ cycles = @timing[item]
125
+ target = Time.now + @warmup
126
+
127
+ while Time.now < target
128
+ item.call_times cycles
129
+ end
130
+ end
131
+ end
132
+
133
+ alias_method :old_initialize, :initialize
134
+
135
+ def initialize opts={}
136
+ old_initialize opts
137
+ @warmup = 60
138
+ @time = 30
139
+ end
140
+
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,13 @@
1
+ def full_playout_for(size)
2
+ playout_object_for(size).play
3
+ end
4
+
5
+ def playout_for(size)
6
+ playout_object = playout_object_for(size)
7
+ playout_object.playout
8
+ playout_object
9
+ end
10
+
11
+ def playout_object_for(size)
12
+ MCTS::Playout.new(Rubykon::GameState.new Rubykon::Game.new(size))
13
+ end
@@ -0,0 +1,22 @@
1
+ require_relative '../lib/mcts'
2
+ require_relative '../lib/mcts/examples/double_step'
3
+
4
+ mcts = MCTS::MCTS.new
5
+
6
+
7
+ [
8
+ {black: -4, white: 0}, {black: -3, white: 0}, {black: -2, white: 0},
9
+ {black: -1, white: 0}, {black: 0, white: 0}, {black: 0, white: -1},
10
+ {black: 0, white: -2}, {black: 0, white: -3},
11
+ {black: 0, white: -4}].each do |position|
12
+ [2, 4, 8, 10, 16, 32, 64, 100].each do |playouts|
13
+ results = Hash.new {0}
14
+ double_step_game = MCTS::Examples::DoubleStep.new position
15
+ 10_000.times do |i|
16
+ root = mcts.start double_step_game, playouts
17
+ best_move = root.best_move
18
+ results[best_move] += 1
19
+ end
20
+ puts "Distribution for #{playouts} playouts with a handicap of #{position[:black] - position[:white]}: #{results}"
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/exe/env ruby
2
+ require_relative '../lib/rubykon'
3
+
4
+ cli = Rubykon::CLI.new
5
+ cli.start
@@ -0,0 +1,14 @@
1
+ require_relative 'avg/job'
2
+ require_relative 'avg/benchmark_suite'
3
+
4
+ module Benchmark
5
+ module Avg
6
+ def avg
7
+ benchmark_suite = BenchmarkSuite.new
8
+ yield benchmark_suite
9
+ benchmark_suite.run
10
+ end
11
+ end
12
+
13
+ extend Benchmark::Avg
14
+ end
@@ -0,0 +1,59 @@
1
+ # encoding: UTF-8
2
+ module Benchmark
3
+ module Avg
4
+
5
+ OUTPUT_WIDTH = 80
6
+ LABEL_WIDTH = 30
7
+ PADDING = 2
8
+ METRICS_WIDTH = OUTPUT_WIDTH - LABEL_WIDTH
9
+
10
+ class BenchmarkSuite
11
+
12
+ def initialize
13
+ @options = default_options
14
+ @jobs = []
15
+ end
16
+
17
+ def config(options)
18
+ @options.merge! options
19
+ end
20
+
21
+ def report(label = "", &block)
22
+ @jobs << Job.new(label, block)
23
+ self
24
+ end
25
+
26
+ def run
27
+ puts 'Running your benchmark...'
28
+ divider
29
+ each_job { |job| job.run @options[:warmup], @options[:time] }
30
+ puts 'Benchmarking finished, here are your reports...'
31
+ puts
32
+ puts 'Warm up results:'
33
+ divider
34
+ each_job { |job| puts job.warmup_report }
35
+ puts
36
+ puts 'Runtime results:'
37
+ divider
38
+ each_job { |job| puts job.runtime_report }
39
+ divider
40
+ end
41
+
42
+ private
43
+ def default_options
44
+ {
45
+ warmup: 30,
46
+ time: 60,
47
+ }
48
+ end
49
+
50
+ def divider
51
+ puts '-' * OUTPUT_WIDTH
52
+ end
53
+
54
+ def each_job(&proc)
55
+ @jobs.each &proc
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,92 @@
1
+ # encoding: UTF-8
2
+ module Benchmark
3
+ module Avg
4
+ class Job
5
+ PRECISION = 2
6
+
7
+ def initialize(label, block)
8
+ @label = label
9
+ @block = block
10
+ @warmup_samples = []
11
+ @run_samples = []
12
+ @warming_up = true
13
+ end
14
+
15
+ def run(warmup_time, run_time)
16
+ warmup_finish = Time.now + warmup_time
17
+ measure_until(@warmup_samples, warmup_finish)
18
+ finish_warmup
19
+
20
+ suite_finish = Time.now + run_time
21
+ measure_until(@run_samples, suite_finish)
22
+ finish_measure
23
+ end
24
+
25
+
26
+ def warmup_report
27
+ report @warmup_samples
28
+ end
29
+
30
+ def runtime_report
31
+ report @run_samples
32
+ end
33
+
34
+ private
35
+ def measure_until(samples, finish_time)
36
+ while Time.now < finish_time do
37
+ measure_block(samples)
38
+ end
39
+ end
40
+
41
+ def measure_block(samples)
42
+ start = Time.now
43
+ @block.call
44
+ finish = Time.now
45
+ samples << (finish - start)
46
+ end
47
+
48
+ def finish_warmup
49
+ @warming_up = false
50
+ puts "Finished warm up for #{@label}, running the real bechmarks now"
51
+ end
52
+
53
+ def finish_measure
54
+ puts "Finished measuring the run time for #{@label}"
55
+ end
56
+
57
+ def report(samples)
58
+ times = extract_times(samples)
59
+ label = @label.ljust(LABEL_WIDTH - PADDING) + padding_space
60
+ metrics = "#{round(times[:ipm])} i/min" << padding_space
61
+ metrics << "#{round(times[:avg])} s (avg)" << padding_space
62
+ metrics << "(± #{round(times[:standard_deviation_percent])}%)"
63
+ label + metrics
64
+ end
65
+
66
+ def round(number)
67
+ # not Float#round to also get numbers like 3.20 (static number after ,)
68
+ sprintf("%.#{PRECISION}f", number)
69
+ end
70
+
71
+ def padding_space
72
+ ' ' * PADDING
73
+ end
74
+
75
+ def extract_times(samples)
76
+ times = {}
77
+ times[:total] = samples.inject(:+)
78
+ iterations = samples.size
79
+ times[:avg] = times[:total] / iterations
80
+ times[:ipm] = iterations / (times[:total] / 60)
81
+ total_variane = samples.inject(0) do |total, time|
82
+ total + ((time - times[:avg]) ** 2)
83
+ end
84
+ times[:variance] = total_variane / iterations
85
+ times[:standard_deviation] = Math.sqrt times[:variance]
86
+ times[:standard_deviation_percent] =
87
+ 100.0 * (times[:standard_deviation] / times[:avg])
88
+ times
89
+ end
90
+ end
91
+ end
92
+ end