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,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