combinatorial_puzzle_solver 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|