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.
- data/.gitignore +10 -0
- data/Gemfile +4 -0
- data/README.md +465 -0
- data/Rakefile +40 -0
- data/bin/setup +7 -0
- data/combinatorial_puzzle_solver.gemspec +28 -0
- data/example_puzzles/4x4 +1 -0
- data/example_puzzles/compile_examples.rb +116 -0
- data/example_puzzles/examples.yaml +243 -0
- data/example_puzzles/hard +11 -0
- data/example_puzzles/medium +12 -0
- data/example_puzzles/simple +9 -0
- data/exe/solve_sudoku +110 -0
- data/lib/combinatorial_puzzle_solver.rb +12 -0
- data/lib/combinatorial_puzzle_solver/constraint.rb +54 -0
- data/lib/combinatorial_puzzle_solver/identifier.rb +55 -0
- data/lib/combinatorial_puzzle_solver/inconsistency.rb +9 -0
- data/lib/combinatorial_puzzle_solver/possibilities.rb +89 -0
- data/lib/combinatorial_puzzle_solver/puzzle.rb +124 -0
- data/lib/combinatorial_puzzle_solver/solution_space.rb +159 -0
- data/lib/combinatorial_puzzle_solver/sudoku.rb +95 -0
- data/lib/combinatorial_puzzle_solver/version.rb +4 -0
- metadata +155 -0
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,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
|