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