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