combinatorial_puzzle_solver 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.
@@ -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: