bfs_brute_force 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d9930b15a9a8a806ab35bd20859d81154d3cd8c2
4
+ data.tar.gz: 5f86c835cef8195335c7c522bce1b4b9b3d60f09
5
+ SHA512:
6
+ metadata.gz: 482eaa39bd9e01336086bb52ef921f811b43166095bb5268db83c0b46a8b3728d924b4e23d9c1419810dc7bcdff9d65ad4aa6a111ad6bb2c35a4148820f2ef67
7
+ data.tar.gz: f4118407512dc990b627e8fb5479de802c89601d73d9f9f6eccefa5514cf50d453995fd8a25c3bbd59519e095427949c361775e82e262e33649f46cd87d148a9
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ bfs_brute_force
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.1.4
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bfs_brute_force.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Joe Sortelli
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,122 @@
1
+ # BfsBruteForce
2
+
3
+ Lazy breadth first brute force search for solutions to puzzles.
4
+
5
+ This ruby gem provides an API for representing the initial state
6
+ and allowed next states of a puzzle, reachable through user defined
7
+ moves. The framework also provides a simple solver which will lazily
8
+ evaluate all the states in a breadth first manner to find a solution
9
+ state, returning the list of moves required to transition from the
10
+ initial state to solution state.
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'bfs_brute_force'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install bfs_brute_force
25
+
26
+ ## Usage
27
+
28
+ Your puzzle must be represented by a subclass of ```BfsBruteForce::State```.
29
+ Each instance of your State subclass must:
30
+
31
+ 1. Store the current state of the puzzle (instance attributes)
32
+ 2. Determine if the state is a win condition of the puzzle (```solved?```)
33
+ 3. Provide a generator for reaching all possible next states (```next_states```)
34
+
35
+ ### Example Puzzle
36
+
37
+ Imagine a simple puzzle where you are given a starting number, an
38
+ ending number, and you can only perform one of three addition
39
+ operations (adding one, ten, or one hundred).
40
+
41
+ To use ```BfsBruteForce``` you will create your
42
+ ```BfsBruteForce::State``` subclass as follows:
43
+ require 'bfs_brute_force'
44
+
45
+ class AdditionPuzzleState < BfsBruteForce::State
46
+ attr_reader :value
47
+
48
+ def initialize(start, final)
49
+ @start = start
50
+ @value = start
51
+ @final = final
52
+ end
53
+
54
+ def solved?
55
+ @value == @final
56
+ end
57
+
58
+ def to_s
59
+ "<#{self.class} puzzle from #{@start} to #{@final}>"
60
+ end
61
+
62
+ def next_states(already_seen)
63
+ return if @value > @final
64
+
65
+ [1, 10, 100].each do |n|
66
+ new_value = @value + n
67
+ if already_seen.add?(new_value)
68
+ yield "Add #{n}", AdditionPuzzleState.new(new_value, @final)
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ Each instance of ```AdditionPuzzleState``` is immutable. The
75
+ ```next_states``` method takes a single argument, which is a ```Set```
76
+ instance, that can be optionally used by your implementation to
77
+ record states that have already been evaluated, as any previously
78
+ evaluated state is already known to not be a solution.
79
+
80
+ Inside of ```next_states``` you should yield two arguments for every
81
+ valid next state of the puzzle:
82
+
83
+ 1. A string, naming the move required to get to the next state
84
+ 2. The next state, as a new instance of your ```BfsBruteForce::State``` class.
85
+
86
+ Now that you have your ```BfsBruteForce::State``` class, you can
87
+ initialize it with your starting puzzle state, and pass it to
88
+ ```BfsBruteForce::Solver#solve```, which will return an object that
89
+ has a ```moves``` method, which returns an array of the move
90
+ names yielded by your ```next_states``` method:
91
+
92
+ solver = BfsBruteForce::Solver.new
93
+ solution = solver.solve(AdditionPuzzleState.new(0, 42))
94
+
95
+ solution.moves.each_with_index do |move, index|
96
+ puts "Move %02d) %s" % [index + 1, move]
97
+ end
98
+
99
+ ## License
100
+
101
+ Copyright (c) 2014 Joe Sortelli
102
+
103
+ MIT License
104
+
105
+ Permission is hereby granted, free of charge, to any person obtaining
106
+ a copy of this software and associated documentation files (the
107
+ "Software"), to deal in the Software without restriction, including
108
+ without limitation the rights to use, copy, modify, merge, publish,
109
+ distribute, sublicense, and/or sell copies of the Software, and to
110
+ permit persons to whom the Software is furnished to do so, subject to
111
+ the following conditions:
112
+
113
+ The above copyright notice and this permission notice shall be
114
+ included in all copies or substantial portions of the Software.
115
+
116
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
117
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
118
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
119
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
120
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
121
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
122
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.test_files = FileList['test/*.rb']
6
+ end
7
+
8
+ task :build => :test
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bfs_brute_force/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "bfs_brute_force"
8
+ spec.version = BfsBruteForce::VERSION
9
+ spec.authors = ["Joe Sortelli"]
10
+ spec.email = ["joe@sortelli.com"]
11
+ spec.summary = "Lazy breadth first brute force search for solutions to puzzles"
12
+ spec.description = %q{
13
+ Provides an API for representing the initial state and allowed
14
+ next states of a puzzle, reachable through user defined moves.
15
+ The framework also provides a simple solver which will lazily
16
+ evaluate all the states in a breadth first manner to find a
17
+ solution state, returning the list of moves required to transition
18
+ from the initial state to solution state.
19
+ }
20
+ spec.homepage = "https://github.com/sortelli/bfs_brute_force"
21
+ spec.license = "MIT"
22
+
23
+ spec.files = `git ls-files -z`.split("\x0")
24
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
25
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.7"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "minitest", "~> 4.7"
31
+ end
@@ -0,0 +1,3 @@
1
+ module BfsBruteForce
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,145 @@
1
+ require "bfs_brute_force/version"
2
+ require "set"
3
+
4
+ # Top level module for this framework
5
+ module BfsBruteForce
6
+ # Exception thrown by {Solver#solve}
7
+ class NoSolution < StandardError
8
+ # @param num_of_solutions_tried [Fixnum] number of solutions previously tried
9
+ def initialize(num_of_solutions_tried)
10
+ super("No solution in #{num_of_solutions_tried} tries. There are no more states to analyze")
11
+ end
12
+ end
13
+
14
+ # Context object that contains a State and a list of moves required
15
+ # to reach that State from the initial State.
16
+ #
17
+ # @!attribute state [r]
18
+ # @return [State] the current state
19
+ #
20
+ # @!attribute moves [r]
21
+ # @return [Array] the list of moves to this state from
22
+ # the initial state
23
+ class Context
24
+ attr_reader :state, :moves
25
+
26
+ # @param state [State] current state
27
+ # @param already_seen [Set] set of states already processed
28
+ # @param moves [Array] list of moves to get to this state
29
+ def initialize(state, already_seen = Set.new, moves = [])
30
+ @state = state
31
+ @already_seen = already_seen
32
+ @moves = moves
33
+ end
34
+
35
+ # Check if current state is a solution
36
+ # @return [Boolean]
37
+ def solved?
38
+ @state.solved?
39
+ end
40
+
41
+ # Generate all contexts that can be reached from this current context
42
+ # @return [void]
43
+ # @yieldparam next_context [Context] next context
44
+ def next_contexts
45
+ @state.next_states(@already_seen) do |next_move, next_state|
46
+ yield Context.new(next_state, @already_seen, @moves + [next_move])
47
+ end
48
+ end
49
+ end
50
+
51
+ # Single state in a puzzle. Represent your puzzle as
52
+ # a subclass of {State}.
53
+ #
54
+ # @abstract Override {#next_states}, {#to_s} and {#solved?}
55
+ class State
56
+ # Your implementation should yield a (move,state) pair for every
57
+ # state reachable by the current state.
58
+ #
59
+ # You should make use of the already_seen set to only yield
60
+ # states that have not previously been yielded.
61
+ #
62
+ # @example Use already_seen to only yield states not already yielded
63
+ # def next_states(already_seen)
64
+ # next_value = @my_value + 100
65
+ #
66
+ # # See {Set#add?}. Returns nil value is already in the Set.
67
+ # if already_seen.add?(next_value)
68
+ # yield "Add 100", MyState.new(next_value)
69
+ # end
70
+ # end
71
+ #
72
+ # @param already_seen [Set] Set of all already processed states
73
+ #
74
+ # @yield [move, state]
75
+ # @yieldparam move [#to_s] Text description of a state transition
76
+ # @yieldparam state [State] New state, reachable from current state with
77
+ # the provided move
78
+ #
79
+ # @raise [NotImplementedError] if you failed to provide your own implementation
80
+ # @return [void]
81
+ def next_states(already_seen)
82
+ raise NotImplementedError, "next_states is not implemented yet"
83
+ end
84
+
85
+ # Returns true if current state is a solution to the puzzle.
86
+ #
87
+ # @raise [NotImplementedError] if you failed to provide your own implementation
88
+ # @return [Boolean]
89
+ def solved?
90
+ raise NotImplementedError, "solved? is not implemented yet"
91
+ end
92
+ end
93
+
94
+ # Lazy breadth first puzzle solver
95
+ class Solver
96
+ # Find a list of moves from the starting state of the puzzle to a solution state.
97
+ #
98
+ # @param initial_state [State] Initial state of your puzzle
99
+ # @param status [#<<] IO object to receive status messages
100
+ #
101
+ # @raise [NoSolution] No solution is found
102
+ # @return [Context] Solved Context object has the final {State} and list of moves
103
+ def solve(initial_state, status = $stdout)
104
+ status << "Looking for solution for:\n#{initial_state}\n\n"
105
+
106
+ initial_context = Context.new(initial_state)
107
+
108
+ if initial_context.solved?
109
+ status << "Good news, its already solved\n"
110
+ return initial_context
111
+ end
112
+
113
+ tries = 0
114
+ contexts = [initial_context]
115
+
116
+ until contexts.empty?
117
+ status << ("Checking for solutions that take %4d moves ... " % [
118
+ contexts.first.moves.size + 1
119
+ ])
120
+
121
+ new_contexts = []
122
+
123
+ contexts.each do |current_context|
124
+ current_context.next_contexts do |context|
125
+ tries += 1
126
+
127
+ if context.solved?
128
+ status << "solved in #{tries} tries\n\nMoves:\n"
129
+ context.moves.each {|m| status << " #{m}\n"}
130
+ status << "\nFinal state:\n #{context.state}\n"
131
+ return context
132
+ end
133
+
134
+ new_contexts << context
135
+ end
136
+ end
137
+
138
+ contexts = new_contexts
139
+ status << ("none in %9d new states\n" % contexts.size)
140
+ end
141
+
142
+ raise NoSolution.new(tries)
143
+ end
144
+ end
145
+ end
data/test/basic.rb ADDED
@@ -0,0 +1,46 @@
1
+ require "minitest/autorun"
2
+ require "bfs_brute_force"
3
+
4
+ class AlreadySolvedState < BfsBruteForce::State
5
+ def solved?
6
+ true
7
+ end
8
+ end
9
+
10
+ class BrokenState < BfsBruteForce::State
11
+ def solved?
12
+ false
13
+ end
14
+ end
15
+
16
+ class TestBasic < Minitest::Unit::TestCase
17
+ def test_module_exists
18
+ mod_key = :BfsBruteForce
19
+ assert Kernel.const_defined?(mod_key), "Module #{mod_key} missing"
20
+
21
+ mod = Kernel.const_get mod_key
22
+ %w{Context State Solver}.each do |c|
23
+ assert mod.const_defined?(c), "Class #{mod}::#{c} missing"
24
+ end
25
+ end
26
+
27
+ def test_already_solved
28
+ state = AlreadySolvedState.new
29
+ solver = BfsBruteForce::Solver.new
30
+
31
+ assert_raises(NotImplementedError) {state.next_states(nil)}
32
+ assert state.solved?
33
+
34
+ solver.solve state, []
35
+ end
36
+
37
+ def test_broken
38
+ state = BrokenState.new
39
+ solver = BfsBruteForce::Solver.new
40
+
41
+ assert_raises(NotImplementedError) {state.next_states(nil)}
42
+ refute state.solved?
43
+
44
+ assert_raises(NotImplementedError) { solver.solve(state, []) }
45
+ end
46
+ end
@@ -0,0 +1,91 @@
1
+ require "minitest/autorun"
2
+ require "bfs_brute_force"
3
+
4
+ class SimplePuzzleState < BfsBruteForce::State
5
+ attr_reader :value
6
+
7
+ def initialize(start, final)
8
+ @start = start
9
+ @value = start
10
+ @final = final
11
+ end
12
+
13
+ def solved?
14
+ @value == @final
15
+ end
16
+
17
+ def to_s
18
+ "<#{self.class} puzzle from #{@start} to #{@final}>"
19
+ end
20
+
21
+ def next_states(already_seen)
22
+ return if @value > @final
23
+
24
+ [1, 10, 100].each do |n|
25
+ new_value = @value + n
26
+ if already_seen.add?(new_value)
27
+ yield "Add #{n}", SimplePuzzleState.new(new_value, @final)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ class SlightlyHarderPuzzleState < SimplePuzzleState
34
+ def next_states(already_seen)
35
+ [10, 100].each do |n|
36
+ new_value = @value + n
37
+ if already_seen.add?(new_value)
38
+ yield "Add #{n}", SlightlyHarderPuzzleState.new(new_value, @final)
39
+ end
40
+ end
41
+
42
+ new_value = @value - 1
43
+ if already_seen.add?(new_value)
44
+ yield "Subtract 1", SlightlyHarderPuzzleState.new(new_value, @final)
45
+ end
46
+ end
47
+ end
48
+
49
+ class TestSimplePuzzle < Minitest::Unit::TestCase
50
+ def test_simple_puzzle
51
+ [
52
+ [0, 42, ["Add 1"] * 2 + ["Add 10"] * 4],
53
+ [2, 42, ["Add 10"] * 4],
54
+ [3, 427, ["Add 1"] * 4 + ["Add 10"] * 2 + ["Add 100"] * 4]
55
+ ].each do |args|
56
+ solve_puzzle(SimplePuzzleState, *args)
57
+ end
58
+
59
+ assert_raises(BfsBruteForce::NoSolution) do
60
+ solve_puzzle(SimplePuzzleState, 3, 2, [])
61
+ end
62
+ end
63
+
64
+ def test_slightly_harder_puzzle
65
+ [
66
+ [0, 42, ["Add 10"] * 5 + ["Subtract 1"] * 8],
67
+ [2, 42, ["Add 10"] * 4],
68
+ [3, 427, ["Add 10"] * 3 + ["Add 100"] * 4 + ["Subtract 1"] * 6]
69
+ ].each do |args|
70
+ solve_puzzle(SlightlyHarderPuzzleState, *args)
71
+ end
72
+ end
73
+
74
+ def solve_puzzle(type, start, final, expected_moves)
75
+ state = type.new(start, final)
76
+ solver = BfsBruteForce::Solver.new
77
+
78
+ refute state.solved?, "Not already solved"
79
+
80
+ context = solver.solve state
81
+
82
+ assert_instance_of(BfsBruteForce::Context, context)
83
+ assert_instance_of(type, context.state)
84
+ assert_instance_of(Array, context.moves)
85
+
86
+ assert context.solved?
87
+ assert context.state.solved?
88
+ assert_equal context.state.value, final
89
+ assert_equal expected_moves, context.moves
90
+ end
91
+ end
metadata ADDED
@@ -0,0 +1,104 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bfs_brute_force
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joe Sortelli
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-12-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.7'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '4.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '4.7'
55
+ description: "\n Provides an API for representing the initial state and allowed\n
56
+ \ next states of a puzzle, reachable through user defined moves.\n The framework
57
+ also provides a simple solver which will lazily\n evaluate all the states in
58
+ a breadth first manner to find a\n solution state, returning the list of moves
59
+ required to transition\n from the initial state to solution state.\n "
60
+ email:
61
+ - joe@sortelli.com
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - ".gitignore"
67
+ - ".ruby-gemset"
68
+ - ".ruby-version"
69
+ - Gemfile
70
+ - LICENSE.txt
71
+ - README.md
72
+ - Rakefile
73
+ - bfs_brute_force.gemspec
74
+ - lib/bfs_brute_force.rb
75
+ - lib/bfs_brute_force/version.rb
76
+ - test/basic.rb
77
+ - test/simple_puzzle.rb
78
+ homepage: https://github.com/sortelli/bfs_brute_force
79
+ licenses:
80
+ - MIT
81
+ metadata: {}
82
+ post_install_message:
83
+ rdoc_options: []
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ required_rubygems_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ requirements: []
97
+ rubyforge_project:
98
+ rubygems_version: 2.4.4
99
+ signing_key:
100
+ specification_version: 4
101
+ summary: Lazy breadth first brute force search for solutions to puzzles
102
+ test_files:
103
+ - test/basic.rb
104
+ - test/simple_puzzle.rb