combinatorial_puzzle_solver 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,159 @@
1
+
2
+ module CombinatorialPuzzleSolver
3
+
4
+ # A mapping between each identifier of a puzzle and their possible values.
5
+ class SolutionSpace < Hash
6
+
7
+ # @return [Puzzle] the Puzzle that this solution space is associated with.
8
+ attr_reader :puzzle
9
+
10
+ # Creates a solution space for a given puzzle.
11
+ # @param puzzle [Puzzle,SolutionSpace] the puzzle that this solution space is
12
+ # based on, or a clone if a SolutionSpace is
13
+ # given.
14
+ def initialize(puzzle)
15
+ if puzzle.is_a?(Puzzle) then
16
+ @puzzle = puzzle
17
+ @puzzle.identifiers.each{|identifier|
18
+ self[identifier] = Possibilities.new(self, identifier)
19
+ }
20
+
21
+ select{|identifier| identifier.has_value? }.each{|identifier, possibilities|
22
+ possibilities.dependent_identifiers_cannot_be!(identifier.value)
23
+ }
24
+ elsif puzzle.is_a?(SolutionSpace)
25
+ puzzle.each{|identifier, possibilities|
26
+ clone = Possibilities.new(self, identifier)
27
+ clone.clear
28
+ clone.concat(possibilities)
29
+ self[identifier] = clone
30
+ }
31
+ @puzzle = puzzle.puzzle
32
+ end
33
+ end
34
+
35
+ # @return [Hash<Identifier,Object>] a Hash of all identifiers that are resolved,
36
+ # mapped to the value they are resolved to.
37
+ def resolved_identifiers
38
+ resolved = {}
39
+ each{|identifier, possibilities|
40
+ if !identifier.has_value? && possibilities.resolved? then
41
+ resolved[identifier] = possibilities.first
42
+ end
43
+ }
44
+ resolved
45
+ end
46
+
47
+ # @return [Hash<Identifier,Possibilities>] the unresolved identifiers and their
48
+ # possible values.
49
+ def unresolved_identifiers
50
+ select{|identifier, possibilities|
51
+ !identifier.has_value? && !possibilities.resolved?
52
+ }
53
+ end
54
+
55
+ # @return [Hash<Identifier,Object>] a Hash of all identifiers that can be
56
+ # resolved by iterating possible values of each
57
+ # constraint.
58
+ def resolvable_from_constraints
59
+ resolvable=Hash.new
60
+ @puzzle.constraints.each{|constraint|
61
+ constraint.resolvable_identifiers(self, resolvable)
62
+ }
63
+ resolvable
64
+ end
65
+
66
+ # Resolves the given implications and the new implications they yield (including
67
+ # those resolved from constraints, if no implications are given) until no more
68
+ # identifiers can be resolved.
69
+ #
70
+ # If the solution space does not become {#resolved?} by this, it is necessary to
71
+ # {#resolve_by_trial_and_error!} to find a complete solution.
72
+ #
73
+ # If a block is given it will yield the solution space with the corresponding
74
+ # identifier and value before each time a resolution is made
75
+ # ({Possibilities#must_be!}), and it will abort unless the block returns true.
76
+ #
77
+ # @param implications [Hash<Identifier,Object>] a mapping between identifiers and
78
+ # their assumed value.
79
+ # @yieldparam solution_space [SolutionSpace] the solution space in the state
80
+ # right before a resolution is made.
81
+ # @yieldparam identifier [Identifier] an identifier that is resolved.
82
+ # @yieldparam value [Object] the value the identifier is resolved to.
83
+ # @yieldreturn [true,nil] the resolution will abort unless the block returns true
84
+ # for each identifier that is resolved.
85
+ # @return [true,false] true if the solution space became completely resolved.
86
+ # @raise [Inconsistency] if the solution space becomes inconsistent without any
87
+ # possible solution.
88
+ def resolve!(implications)
89
+
90
+ loop do
91
+ resolvable = Hash.new
92
+
93
+ implications.each{|identifier, value|
94
+ if block_given? then
95
+ return false unless yield(self, identifier, value) == true
96
+ end
97
+ self[identifier].must_be!(value, resolvable)
98
+ }
99
+
100
+ if resolvable.empty? then
101
+ implications = resolvable_from_constraints
102
+ else
103
+ implications = resolvable
104
+ end
105
+
106
+
107
+ break if implications.empty?
108
+ end
109
+
110
+ resolved?
111
+ end
112
+
113
+ # Iterates the possible values of unresolved identifiers and returns the solution
114
+ # space for the first attempt that yields a complete solution. It will return
115
+ # itself if it becomes resolved by eliminating the attempts that failed.
116
+ #
117
+ # @return [SolutionSpace] a solution space that is completely resolved
118
+ # @raise [Inconsistency] if no complete solution is found.
119
+ def resolve_by_trial_and_error!
120
+ unresolved_identifiers.each{|identifier, possibilities|
121
+ until possibilities.resolved? do
122
+ maybe_value = possibilities.first
123
+ maybe_solution = try_resolve_with(identifier, maybe_value)
124
+ return maybe_solution unless maybe_solution.nil?
125
+
126
+ resolve!(self[identifier].cannot_be!(maybe_value))
127
+ end
128
+ }
129
+
130
+ self
131
+ end
132
+
133
+ # Creates a clone of the solution space and tries to solve it with the assumption
134
+ # that the given identifier has the given value.
135
+ # @return [SolutionSpace, nil] a solution space that is completely resolved, or
136
+ # nil if no complete solution was found.
137
+ def try_resolve_with(identifier, value)
138
+ possible_solution_space = SolutionSpace.new(self)
139
+ assumption = {identifier => value}
140
+
141
+ begin
142
+ if possible_solution_space.resolve!(assumption) then
143
+ return possible_solution_space
144
+ else
145
+ return possible_solution_space.resolve_by_trial_and_error!
146
+ end
147
+ rescue Inconsistency
148
+ return nil
149
+ end
150
+ end
151
+
152
+ # @return [true, false] true if all identifiers have been resolved.
153
+ def resolved?
154
+ all?{|identifier,possibilities|
155
+ identifier.has_value? || possibilities.resolved?
156
+ }
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,95 @@
1
+
2
+ module CombinatorialPuzzleSolver
3
+
4
+
5
+ # A sudoku puzzle.
6
+ #
7
+ # A puzzle with dimension 3 implies the standard 9x9 puzzle, whereas dimension 2
8
+ # implies a 4x4 puzzle.
9
+ class Sudoku < Puzzle
10
+
11
+ # @return [Fixnum] the width/height of the sudoku board (9 for a 9x9 puzzle).
12
+ attr_reader :size
13
+
14
+ # Parses a string where 1-9 are interpreted as values and 0 is interpreted as
15
+ # unassigned, whereas all other characters (including whitespaces) are
16
+ # discarded.
17
+ #
18
+ # @param string [String] the input string.
19
+ # @param dimension [Fixnum] Dimension 2 implies a 4x4 puzzle, 3 implies 9x9.
20
+ # @yieldparam sudoku [Sudoku] each parsed sudoku puzzle.
21
+ # @return [Array[Sudoku]] the parsed sudoku puzzles.
22
+ # @raise [RuntimeError] if the parsed digits don't match up to even puzzles.
23
+ def self.scan(string, dimension=3)
24
+ digits = string.scan(/\d/).collect{|value| value.to_i}
25
+ size = dimension**4
26
+ digits.each_slice(size).collect{|digits|
27
+ puzzle = Sudoku.new(dimension, digits)
28
+ yield puzzle if block_given?
29
+ puzzle
30
+ }
31
+ end
32
+
33
+ # Creates a new and empty sudoku puzzle, with all identifiers unassigned.
34
+ # @param dimension [Fixnum] the dimension of the puzzle ('2' implies a 4x4 puzzle
35
+ # and '3' implies a 9x9 puzzle).
36
+ # @param digits [Array[Fixnum]] an optional array of digits, where each digit
37
+ # corresponds to an identifier's value from 0 to 9.
38
+ # @raise [RuntimeError] if the digits array (if given) is of incorrect size.
39
+ def initialize(dimension=3, digits=[])
40
+ @dimension = dimension
41
+ @size = dimension*dimension
42
+ super(@size*@size, (1..@size).to_a) {|identifier|
43
+ (rows + columns + squares).collect{|group| Constraint.new(group) }
44
+ }
45
+
46
+ unless digits.empty? then
47
+ unless digits.size == @size*@size then
48
+ error_msg = "Parsed #{digits.size} digits instead of #{@size*@size})."
49
+ raise RuntimeError.new(error_msg)
50
+ end
51
+
52
+ digits.each_with_index{|value, index|
53
+ identifiers[index].set!(value) unless value == 0
54
+ }
55
+ end
56
+ end
57
+
58
+ # @return [Array<Array<Identifier>>] the identifiers grouped by rows
59
+ def rows
60
+ @identifiers.each_slice(@size).to_a
61
+ end
62
+
63
+ # @return [Array<Array<Identifier>>] the identifiers grouped by columns
64
+ def columns
65
+ rows.transpose
66
+ end
67
+
68
+ # @return [Array<Array<Identifier>>] the identifiers grouped by squares
69
+ def squares
70
+ slices = @identifiers.each_slice(@dimension).each_slice(@dimension).to_a
71
+ slices.transpose.flatten.each_slice(@size).to_a
72
+ end
73
+
74
+ # @return [String] a simple string representation of the sudoku puzzle.
75
+ def to_s
76
+ horizontal = "\n#{Array.new(@dimension){"-" * (@dimension*2-1) }.join("+")}\n"
77
+ divisors = [horizontal, "\n", "|", " "]
78
+
79
+ strings = @identifiers.collect{|id| (id.has_value?) ? id.value.to_s : " "}
80
+ while divisor = divisors.pop do
81
+ strings = strings.each_slice(@dimension).collect{|s| s.join(divisor) }
82
+ end
83
+ strings.join
84
+ end
85
+
86
+ # @return [String] a string representation of an identifier, which would be
87
+ # "[row,column]"
88
+ def identifier_to_s(identifier)
89
+ index = identifiers.index(identifier)
90
+ "[#{index / size + 1},#{index % size + 1}]"
91
+ end
92
+
93
+
94
+ end
95
+ end
@@ -0,0 +1,4 @@
1
+ module CombinatorialPuzzleSolver
2
+ # the current version
3
+ VERSION = "0.1.0"
4
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: combinatorial_puzzle_solver
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Erik Schlyter
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2015-04-22 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.8'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.8'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '10.0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '10.0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec-illustrate
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 0.1.3
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.1.3
62
+ - !ruby/object:Gem::Dependency
63
+ name: yard
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 0.8.7.6
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 0.8.7.6
78
+ - !ruby/object:Gem::Dependency
79
+ name: redcarpet
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 3.2.2
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: 3.2.2
94
+ description: A resolver of combinatorial number-placement puzzles, like Sudoku.
95
+ email:
96
+ - erik@erisc.se
97
+ executables:
98
+ - solve_sudoku
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - .gitignore
103
+ - Gemfile
104
+ - README.md
105
+ - Rakefile
106
+ - bin/setup
107
+ - combinatorial_puzzle_solver.gemspec
108
+ - example_puzzles/4x4
109
+ - example_puzzles/compile_examples.rb
110
+ - example_puzzles/examples.yaml
111
+ - example_puzzles/hard
112
+ - example_puzzles/medium
113
+ - example_puzzles/simple
114
+ - exe/solve_sudoku
115
+ - lib/combinatorial_puzzle_solver.rb
116
+ - lib/combinatorial_puzzle_solver/constraint.rb
117
+ - lib/combinatorial_puzzle_solver/identifier.rb
118
+ - lib/combinatorial_puzzle_solver/inconsistency.rb
119
+ - lib/combinatorial_puzzle_solver/possibilities.rb
120
+ - lib/combinatorial_puzzle_solver/puzzle.rb
121
+ - lib/combinatorial_puzzle_solver/solution_space.rb
122
+ - lib/combinatorial_puzzle_solver/sudoku.rb
123
+ - lib/combinatorial_puzzle_solver/version.rb
124
+ homepage: https://github.com/ErikSchlyter/combinatorial_puzzle_solver
125
+ licenses: []
126
+ post_install_message:
127
+ rdoc_options: []
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ none: false
132
+ requirements:
133
+ - - ! '>='
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ segments:
137
+ - 0
138
+ hash: 3113729172667928250
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ none: false
141
+ requirements:
142
+ - - ! '>='
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ segments:
146
+ - 0
147
+ hash: 3113729172667928250
148
+ requirements: []
149
+ rubyforge_project:
150
+ rubygems_version: 1.8.23
151
+ signing_key:
152
+ specification_version: 3
153
+ summary: A resolver of combinatorial number-placement puzzles, like Sudoku.
154
+ test_files: []
155
+ has_rdoc: