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,12 @@
1
+ 000|301|000
2
+ 009|000|000
3
+ 080|000|030
4
+ ---+---+---
5
+ 000|004|980
6
+ 007|120|500
7
+ 200|090|170
8
+ ---+---+---
9
+ 050|012|000
10
+ 090|007|000
11
+ 300|405|008
12
+
@@ -0,0 +1,9 @@
1
+ 906813540
2
+ 201045063
3
+ 040000000
4
+ 000620009
5
+ 009000200
6
+ 700034000
7
+ 000000090
8
+ 590360104
9
+ 027459306
data/exe/solve_sudoku ADDED
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'combinatorial_puzzle_solver'
4
+
5
+ def parse_options(argv)
6
+ options = { :stderr => $stderr }
7
+
8
+ option_parser = OptionParser.new { |opts|
9
+ opts.banner = "Usage: solve_sudoku [options] [files]"
10
+
11
+ options[:dimension] = 3
12
+
13
+ opts.on("-4", "--4x4", "Assume puzzles are 4x4 instead of 9x9") {|dimension|
14
+ options[:dimension] = 2
15
+ }
16
+
17
+ opts.on("-c", "--clues [NUM=1]", Integer,
18
+ "Abort after NUM steps, don't solve entire puzzle.") {|clues|
19
+ options[:clues] = clues || 1
20
+ }
21
+
22
+ opts.on("-r", "--resolution-only", "Avoid trial and error",
23
+ "Might not reach a complete solution.") {|res|
24
+ options[:resolution] = res
25
+ }
26
+
27
+ opts.separator ""
28
+ opts.separator "Output:"
29
+
30
+ opts.on("-i", "--print-input", "Print the parsed input puzzles."){|input|
31
+ options[:parsed_stream] = $stdout
32
+ }
33
+
34
+ opts.on("-o", "--print-output", "Print the result puzzles."){|output|
35
+ options[:result_stream] = $stdout
36
+ }
37
+
38
+ opts.on("-s", "--print-steps", "Print each resolution step.",
39
+ "It will not print steps resolved by trial and error."){|steps|
40
+ options[:steps_stream] = $stdout
41
+ }
42
+
43
+ opts.on("-p", "--print-puzzle-steps",
44
+ "Print entire puzzle for each resolution step.",
45
+ "It will not print steps resolved by trial and error."){|steps|
46
+ options[:puzzle_stream] = $stdout
47
+ }
48
+
49
+ opts.separator ""
50
+ opts.separator "Utility:"
51
+
52
+ opts.on_tail("-h", "--help", "Show this message.") {
53
+ $stderr.puts opts
54
+ return nil
55
+ }
56
+ }
57
+
58
+ begin
59
+ option_parser.parse!(argv)
60
+ rescue OptionParser::InvalidOption => e
61
+ $stderr.puts option_parser.help
62
+ return nil
63
+ end
64
+
65
+ options
66
+ end
67
+
68
+ # Scans the input string and resolves the sudoku puzzles it manages to parse.
69
+ #
70
+ # @param input [String] the string to be parsed
71
+ # @param options [Hash] the options parsed from ARGV, along with output streams for diagnostic purposes.
72
+ # @return [true,false] whether all the puzzles was completely resolved or not.
73
+ def scan_and_resolve!(input, options)
74
+ success = true
75
+ begin
76
+ CombinatorialPuzzleSolver::Sudoku.scan(input, options[:dimension]){|sudoku|
77
+ begin
78
+ success &= sudoku.resolve!(!options[:resolution], options[:clues], options)
79
+
80
+ rescue CombinatorialPuzzleSolver::Inconsistency => error
81
+ options[:stderr].puts "Puzzle inconsistent, no solution possible."
82
+ success = false
83
+ end
84
+ }
85
+ rescue => error
86
+ options[:stderr].puts error
87
+ success = false
88
+ end
89
+ success
90
+ end
91
+
92
+
93
+
94
+ options = parse_options(ARGV)
95
+ exit 1 if options.nil?
96
+
97
+ if ARGV.empty? then
98
+ exit scan_and_resolve!($stdin.read, options) ? 0 : 1
99
+ else
100
+ success = true
101
+ ARGV.each{|filename|
102
+ begin
103
+ success &= scan_and_resolve!(File.read(filename), options)
104
+ rescue IOError => error
105
+ $stderr.puts "Failed reading #{filename}, #{error}."
106
+ end
107
+ }
108
+ exit success ? 0 : 1
109
+ end
110
+
@@ -0,0 +1,12 @@
1
+ require "combinatorial_puzzle_solver/version"
2
+ require "combinatorial_puzzle_solver/puzzle"
3
+ require "combinatorial_puzzle_solver/inconsistency"
4
+ require "combinatorial_puzzle_solver/identifier"
5
+ require "combinatorial_puzzle_solver/constraint"
6
+ require "combinatorial_puzzle_solver/solution_space"
7
+ require "combinatorial_puzzle_solver/possibilities"
8
+ require "combinatorial_puzzle_solver/sudoku"
9
+
10
+ # A resolver of combinatorial number-placement puzzles, like Sudoku.
11
+ module CombinatorialPuzzleSolver
12
+ end
@@ -0,0 +1,54 @@
1
+
2
+ module CombinatorialPuzzleSolver
3
+
4
+ # A collection of identifiers that all should have unique values, like a row or a
5
+ # column in a Sudoku puzzle.
6
+ class Constraint < Array
7
+
8
+ def initialize(identifiers)
9
+ super(identifiers)
10
+ fail unless identifiers.is_a?(Array)
11
+ identifiers.each{|id| fail unless id.is_a?(Identifier)}
12
+
13
+ each{|identifier| identifier.constraints << self }
14
+ end
15
+
16
+ # Iterates all possible values that can be set within this constraint, and
17
+ # when a value can only be placed within that specific identifier.
18
+ #
19
+ # @param solution_space [SolutionSpace] the mapping between the puzzle's
20
+ # identifiers and the values they can have.
21
+ # @param resolvable [Hash<Identifier,Object>] an optional Hash to store all
22
+ # identifiers and values that becomes
23
+ # resolvable.
24
+ # @return [Hash<Identifier,Object>] the identifiers that can be resolved, mapped
25
+ # to the only value they can have.
26
+ def resolvable_identifiers(solution_space, resolvable=Hash.new)
27
+ possible_values(solution_space).each{|value, identifiers|
28
+ if identifiers.size == 1 then
29
+ resolved_identifier = identifiers.first
30
+ unless solution_space[resolved_identifier].resolved? then
31
+ resolvable[resolved_identifier] = value
32
+ end
33
+ end
34
+ }
35
+ resolvable
36
+ end
37
+
38
+ # @param solution_space [SolutionSpace] the mapping between the puzzle's
39
+ # identifiers and the values they can have.
40
+ # @return [Hash<Object,Set<Identifier>>] a map between each possible value within
41
+ # this constraint and the set of
42
+ # identifiers that can have them.
43
+ def possible_values(solution_space)
44
+ values = Hash.new{|value,ids| value[ids] = Set.new }
45
+
46
+ each{|identifier|
47
+ solution_space[identifier].each{|value|
48
+ values[value] << identifier
49
+ }
50
+ }
51
+ values
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,55 @@
1
+
2
+ module CombinatorialPuzzleSolver
3
+
4
+ # Designates the smallest unit that should be mapped to a number, e.g. a cell
5
+ # in a Sudoku puzzle.
6
+ class Identifier
7
+ # @return [Object, nil] the value that this identifier is assigned to, or nil.
8
+ attr_reader :value
9
+
10
+ # @return [Array<Constraint>] the constraints that affect this identifier
11
+ attr_reader :constraints
12
+
13
+ # @return [Puzzle] the puzzle that this identifier belongs to.
14
+ attr_reader :puzzle
15
+
16
+ # Creates an Identifier
17
+ # @param puzzle [Puzzle] the puzzle this identifier belongs to.
18
+ def initialize(puzzle)
19
+ @puzzle = puzzle;
20
+ @value = nil
21
+ @constraints = []
22
+ end
23
+
24
+ # Sets the value for this identifier
25
+ # @param value [Object] the value to set.
26
+ def set!(value)
27
+ @value = value
28
+ end
29
+
30
+ # @return [Array<Identifier>] all other identifiers that are covered by this
31
+ # identifier's constraints.
32
+ def dependent_identifiers
33
+ @constraints.flatten.uniq - [self]
34
+ end
35
+
36
+ # @return [true,false] true if this identifier has a value, false otherwise.
37
+ def has_value?
38
+ !@value.nil?
39
+ end
40
+
41
+ # @return [String] a string representation of this identifier, which is defined
42
+ # by {Puzzle#identifier_to_s}.
43
+ # @see Puzzle#identifier_to_s
44
+ def to_s
45
+ @puzzle.identifier_to_s(self)
46
+ end
47
+
48
+ # @return [String] a string representation of this identifier, which is defined
49
+ # by {Puzzle#inspect_identifier}.
50
+ # @see Puzzle#identifier_to_s
51
+ def inspect
52
+ @puzzle.inspect_identifier(self)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,9 @@
1
+
2
+ module CombinatorialPuzzleSolver
3
+
4
+ # The current state of the solution space has become inconsistent and unresolvable.
5
+ class Inconsistency < RuntimeError
6
+ end
7
+
8
+ end
9
+
@@ -0,0 +1,89 @@
1
+
2
+ module CombinatorialPuzzleSolver
3
+
4
+ # A collection of the possible values an identifier can have. It is reduced by
5
+ # invoking {#cannot_be!} and {#must_be!} until it only has one possible value, and
6
+ # thus becomes {#resolved?}.
7
+ class Possibilities < Array
8
+
9
+ # Creates a set of possibilities for a specific identifier, which initially would
10
+ # be all possible symbols of the puzzle.
11
+ # @param solution_space [SolutionSpace] the solution space that this set belongs
12
+ # to.
13
+ # @param identifier [Identifier] the identifier associated with this set of
14
+ # possible values.
15
+ def initialize(solution_space, identifier)
16
+ @solution_space = solution_space
17
+ @identifier = identifier
18
+
19
+ super(identifier.puzzle.symbols) unless identifier.has_value?
20
+ end
21
+
22
+ # Reduce the possible values by stating that the identifier can only have a
23
+ # certain value.
24
+ #
25
+ # @param value [Object] the only possible value the identifier can have.
26
+ # @param resolvable [Hash<Identifier,Object>] an optional Hash to store all
27
+ # identifiers and values that becomes
28
+ # resolvable.
29
+ # @return [Hash<Identifier,Object>] a Hash of identifiers that becomes
30
+ # resolvable because of this action.
31
+ # @raise [Inconsistency] if the solution space becomes inconsistent without any
32
+ # possible solution.
33
+ def must_be!(value, resolvable=Hash.new)
34
+ raise Inconsistency if @identifier.has_value? && @identifier.value != value
35
+ raise Inconsistency unless include?(value)
36
+
37
+ clear
38
+ push(value)
39
+
40
+ dependent_identifiers_cannot_be!(value, resolvable)
41
+ end
42
+
43
+ # Notifies dependent identifiers that they can't have a certain value.
44
+ #
45
+ # @param value [Object] a value that the dependent identifiers cannot have.
46
+ # @param resolvable [Hash<Identifier,Object>] an optional Hash to store all
47
+ # identifiers and values that becomes
48
+ # resolvable.
49
+ # @return [Hash<Identifier,Object>] the hash of identifiers that becomes
50
+ # resolvable because of this action.
51
+ # @raise [Inconsistency] if the solution space becomes inconsistent without any
52
+ # possible solution.
53
+ def dependent_identifiers_cannot_be!(value, resolvable=Hash.new)
54
+ @identifier.dependent_identifiers.each{|dependency|
55
+ @solution_space[dependency].cannot_be!(value, resolvable)
56
+ }
57
+ resolvable
58
+ end
59
+
60
+ # Reduce the possible values by stating that the identifier cannot have a certain
61
+ # value.
62
+ #
63
+ # @param value [Object] a value that the identifier cannot have.
64
+ # @param resolvable [Hash<Identifier,Object>] an optional Hash to store all
65
+ # identifiers and values that becomes
66
+ # resolvable.
67
+ # @return [Hash<Identifier,Object>] the hash of identifiers that becomes
68
+ # resolvable because of this action.
69
+ # @raise [Inconsistency] if the solution space becomes inconsistent without any
70
+ # possible solution.
71
+ def cannot_be!(value, resolvable=Hash.new)
72
+ raise Inconsistency if @identifier.value == value
73
+
74
+ if delete(value) then
75
+ raise Inconsistency if empty?
76
+ resolvable[@identifier] = first if resolved?
77
+ end
78
+ resolvable
79
+ end
80
+
81
+ # @return [true,false] true if the identifier can only have one possible value,
82
+ # false otherwise.
83
+ def resolved?
84
+ size == 1
85
+ end
86
+ end
87
+
88
+ end
89
+
@@ -0,0 +1,124 @@
1
+ require 'set'
2
+
3
+ module CombinatorialPuzzleSolver
4
+
5
+ # Base class for a combinatorial number-placement puzzle
6
+ class Puzzle
7
+
8
+ # @return [Array<Identifier>] the identifiers of this puzzle.
9
+ attr_reader :identifiers
10
+
11
+ # @return [Array<Constraint>] the constraints of this puzzle.
12
+ attr_reader :constraints
13
+
14
+ # @return [Array] an array that contains all the possible values each
15
+ # identifier can have.
16
+ attr_reader :symbols
17
+
18
+ # Creates a new puzzle.
19
+ #
20
+ # @param identifier_count [Fixnum] The number of identifiers in this puzzle
21
+ # @param symbols [Array] The possible values each identifier can have.
22
+ # @yieldparam identifiers [Array<Identifier>] the identifiers of this puzzle.
23
+ # @yieldreturn [Array<Constraint>] The initialized constraints
24
+ #
25
+ def initialize(identifier_count, symbols, &constraint_block)
26
+
27
+ @symbols = symbols.uniq
28
+ @identifiers = Array.new(identifier_count) { Identifier.new(self) }
29
+
30
+ fail "No block for initiating constraints given" unless block_given?
31
+ @constraints = yield @identifiers
32
+
33
+ fail "The constraint block returned 0 constraints" if @constraints.empty?
34
+ unless @constraints.all?{|constraint| constraint.is_a?(Constraint)} then
35
+ fail "The constraint block may only contain Constraint"
36
+ end
37
+ end
38
+
39
+ # @return [SolutionSpace,nil] the resolved solution space for this puzzle, or nil
40
+ # if the puzzle doesn't have any solution.
41
+ def resolved_solution_space
42
+ begin
43
+ solution_space = SolutionSpace.new(self)
44
+ solution_space.resolve!(solution_space.resolved_identifiers)
45
+ return solution_space.resolve_by_trial_and_error!
46
+ rescue Inconsistency
47
+ nil
48
+ end
49
+ end
50
+
51
+ # @return [String] identifiers and constraints as a string representation.
52
+ # @see Identifier#inspect
53
+ # @see Constraint#inspect
54
+ def inspect
55
+ (@identifiers + @constraints).collect{|obj| obj.inspect }.join("\n")
56
+ end
57
+
58
+ # @return [String] a string representation of an identifier, in the context of
59
+ # this puzzle.
60
+ def identifier_to_s(identifier)
61
+ "[#{identifiers.index(identifier)}]"
62
+ end
63
+
64
+ # @return [String] a string representation of an identifier (including value), in
65
+ # the context of this puzzle.
66
+ def inspect_identifier(identifier)
67
+ if identifier.has_value? then
68
+ identifier_to_s(identifier) << "=#{identifier.value.to_s}"
69
+ else
70
+ identifier_to_s(identifier)
71
+ end
72
+ end
73
+
74
+ # Sets all identifiers to the value they are resolved to.
75
+ # @param solution_space [SolutionSpace]
76
+ def set_resolved_identifiers!(solution_space)
77
+ solution_space.resolved_identifiers.each{|identifier, value|
78
+ identifier.set!(value)
79
+ }
80
+ end
81
+
82
+ # Resolves a puzzle while writing diagnostic output to given streams.
83
+ #
84
+ # @param trial_and_error [Boolean] whether it should resort to trial and error if
85
+ # a complete solution cannot be resolved.
86
+ # @param steps [Fixnum] how many steps it will perform before aborting (or 0).
87
+ # @param output [Hash] a collection of output streams for diagnostic purposes.
88
+ # @return [true,false] whether the puzzle was completely resolved or not.
89
+ # @raise [Inconsistency] if the solution space becomes inconsistent without any
90
+ # possible solution.
91
+ def resolve!(trial_and_error=true, steps=0, output={})
92
+
93
+ # print the parsed puzzle
94
+ output[:parsed_stream].puts to_s << "\n\n" if output[:parsed_stream]
95
+
96
+ step_count = 0
97
+
98
+ solution = SolutionSpace.new(self)
99
+ solution.resolve!(solution.resolved_identifiers){|x, identifier, value|
100
+
101
+ # print the resolved step
102
+ identifier.set!(value)
103
+ output[:steps_stream].puts identifier.inspect if output[:steps_stream]
104
+
105
+ # print the entire board
106
+ output[:puzzle_stream].puts to_s << "\n\n" if output[:puzzle_stream]
107
+
108
+ step_count += 1
109
+ step_count != steps
110
+ }
111
+
112
+ # brute force with trial and error unless we only want the resolution steps
113
+ brute_forced = solution.resolve_by_trial_and_error! if trial_and_error
114
+ solution = brute_forced unless brute_forced.nil?
115
+
116
+ # print the result
117
+ set_resolved_identifiers!(solution)
118
+ output[:result_stream].puts to_s << "\n\n" if output[:result_stream]
119
+
120
+ solution.resolved?
121
+ end
122
+
123
+ end
124
+ end