sudokuhandler 0.1.2 → 0.1.3
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/README.md +104 -113
- data/Rakefile +34 -14
- data/ext/sudoku.c +85 -9
- data/lib/sudoku.rb +33 -197
- data/lib/sudoku/generator.rb +40 -0
- data/lib/sudoku/grid.rb +14 -0
- data/lib/sudoku/logic.rb +242 -0
- data/lib/sudoku/solver.rb +176 -0
- data/lib/sudoku/version.rb +1 -1
- data/tests/test_sudoku.rb +144 -41
- metadata +35 -5
@@ -0,0 +1,176 @@
|
|
1
|
+
module Sudoku
|
2
|
+
#Methodes de resolution des sudokus
|
3
|
+
module Solver
|
4
|
+
#Renvoie les nombres manquants dans le carré comprenant la case x,y
|
5
|
+
# @param [Fixnum] x La colonne d'une case du carré à traiter
|
6
|
+
# @param [Fixnum] y La rangée d'une case du carré à traiter
|
7
|
+
# @return [Array] Les nombres manquants
|
8
|
+
def missing_square x, y
|
9
|
+
Array.new(size){|i| i+1} - square(x,y)
|
10
|
+
end
|
11
|
+
|
12
|
+
#Renvoie les nombres manquants dans la colonne
|
13
|
+
# @param [Fixnum] x La colonne à traiter
|
14
|
+
# @return [Array] Les nombres manquants
|
15
|
+
def missing_col x
|
16
|
+
Array.new(size){|i| i+1} - col(x)
|
17
|
+
end
|
18
|
+
|
19
|
+
#Renvoie les nombres manquants dans la colonne
|
20
|
+
# @param [Fixnum] y La ligne à traiter
|
21
|
+
# @return [Array] Les nombres manquants
|
22
|
+
def missing_row y
|
23
|
+
Array.new(size){|i| i+1} - row(y)
|
24
|
+
end
|
25
|
+
|
26
|
+
#Ajoute un nombre chaque fois que c'est la seule possibilité
|
27
|
+
# @return [Fixnum] le nombre de nombres ajoutés dans la grille
|
28
|
+
def solve_uniq_possibilities!
|
29
|
+
res = 0
|
30
|
+
loop do
|
31
|
+
adds = 0
|
32
|
+
each do |x, y, val|
|
33
|
+
next unless val.zero?
|
34
|
+
|
35
|
+
p = possibilities x, y
|
36
|
+
if p.length == 1
|
37
|
+
set x, y, p.first
|
38
|
+
adds += 1
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
break if adds == 0
|
43
|
+
res += adds
|
44
|
+
end
|
45
|
+
res
|
46
|
+
end
|
47
|
+
|
48
|
+
#Ajoute un nombre chaque fois que c'est la seule position possible dans la colonne
|
49
|
+
# @param [Fixnum] x La colonne à traiter
|
50
|
+
# @return [Fixnum] le nombre de nombres ajoutés
|
51
|
+
def solve_col! x
|
52
|
+
adds = 0
|
53
|
+
|
54
|
+
missing_col(x).each do |val|
|
55
|
+
pos = []
|
56
|
+
size.times do |y|
|
57
|
+
next unless get(x,y) == 0
|
58
|
+
pos << [x,y] if valid? x, y, val
|
59
|
+
end
|
60
|
+
if pos.length == 1
|
61
|
+
set pos[0][0], pos[0][1], val
|
62
|
+
adds += 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
adds
|
67
|
+
end
|
68
|
+
|
69
|
+
#Ajoute un nombre chaque fois que c'est la seule position possible dans la ligne
|
70
|
+
# @param [Fixnum] y La ligne à traiter
|
71
|
+
# @return [Fixnum] le nombre de nombres ajoutés
|
72
|
+
def solve_row! y
|
73
|
+
adds = 0
|
74
|
+
|
75
|
+
missing_row(y).each do |val|
|
76
|
+
pos = []
|
77
|
+
size.times do |x|
|
78
|
+
next unless get(x,y) == 0
|
79
|
+
pos << [x,y] if valid? x, y, val
|
80
|
+
end
|
81
|
+
if pos.length == 1
|
82
|
+
set pos[0][0], pos[0][1], val
|
83
|
+
adds += 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
adds
|
88
|
+
end
|
89
|
+
|
90
|
+
#Ajoute un nombre chaque fois que c'est la seule position possible dans le carré
|
91
|
+
# @param [Fixnum] x La colonne d'une case du carré à traiter
|
92
|
+
# @param [Fixnum] y La rangée d'une case du carré à traiter
|
93
|
+
# @return [Fixnum] le nombre de nombres ajoutés
|
94
|
+
def solve_square! xx, yy
|
95
|
+
xmin = xx - (xx%base)
|
96
|
+
ymin = yy - (yy%base)
|
97
|
+
adds = 0
|
98
|
+
|
99
|
+
missing_square(xx, yy).each do |val|
|
100
|
+
pos = []
|
101
|
+
base.times do |i|
|
102
|
+
base.times do |j|
|
103
|
+
x = xmin + i
|
104
|
+
y = ymin + j
|
105
|
+
next unless get(x,y) == 0
|
106
|
+
pos << [x,y] if valid? x, y, val
|
107
|
+
end
|
108
|
+
end
|
109
|
+
if pos.length == 1
|
110
|
+
set pos[0][0], pos[0][1], val
|
111
|
+
adds += 1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
adds
|
116
|
+
end
|
117
|
+
|
118
|
+
#Utilise solve_uniq_possibilities!, solve_col!, solve_row! et solve_square!
|
119
|
+
#tant qu'ils ajoutent des nombres
|
120
|
+
# @return [Fixnum] le nombre de nombres ajoutés
|
121
|
+
def solve_naive!
|
122
|
+
res = 0
|
123
|
+
|
124
|
+
loop do
|
125
|
+
adds = solve_uniq_possibilities!
|
126
|
+
size.times do |i|
|
127
|
+
adds += solve_col! i
|
128
|
+
adds += solve_row! i
|
129
|
+
|
130
|
+
x = (i*base) % size
|
131
|
+
y = (i/base) * base
|
132
|
+
adds += solve_square! x, y
|
133
|
+
end
|
134
|
+
break if adds.zero?
|
135
|
+
res += adds
|
136
|
+
end
|
137
|
+
|
138
|
+
res
|
139
|
+
end
|
140
|
+
|
141
|
+
#Resoud le sudoku par backtracking
|
142
|
+
# @return (Fixnum) le nombre de nombres ajoutés dans la grille
|
143
|
+
def solve_backtrack!
|
144
|
+
res = solve_naive!
|
145
|
+
|
146
|
+
each do |x, y, cur_val|
|
147
|
+
next unless cur_val.zero?
|
148
|
+
p = possibilities x, y
|
149
|
+
p.each do |val|
|
150
|
+
copy = clone
|
151
|
+
copy.set x, y, val
|
152
|
+
adds = copy.solve_backtrack!
|
153
|
+
if copy.complete?
|
154
|
+
self.import copy
|
155
|
+
return res+adds+1
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
res
|
161
|
+
end
|
162
|
+
|
163
|
+
#Enleve les nombres qui sont impossibles de la grille
|
164
|
+
# @return (Fixnum) le nombre de nombres enlevés
|
165
|
+
def remove_impossible!
|
166
|
+
removes = 0
|
167
|
+
each do |x, y, val|
|
168
|
+
unless valid_cell? x, y, val
|
169
|
+
set x, y, 0
|
170
|
+
removes += 1
|
171
|
+
end
|
172
|
+
end
|
173
|
+
removes
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
data/lib/sudoku/version.rb
CHANGED
data/tests/test_sudoku.rb
CHANGED
@@ -1,19 +1,33 @@
|
|
1
1
|
require 'test/unit'
|
2
2
|
require 'sudoku'
|
3
3
|
|
4
|
+
class MyTestCase < Test::Unit::TestCase
|
5
|
+
#Certains Rubys n'ont pas refute...
|
6
|
+
def refute what, *args
|
7
|
+
begin
|
8
|
+
return super(what, *args)
|
9
|
+
rescue NoMethodError => e
|
10
|
+
return assert(!what, args)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_myrefute
|
15
|
+
refute false, "Implementation refute"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
4
19
|
module GridTest
|
5
20
|
def create
|
6
21
|
klass.new base
|
7
22
|
end
|
8
23
|
|
9
|
-
def
|
24
|
+
def test_initialize
|
10
25
|
s = create
|
11
|
-
s.each{|x,
|
12
|
-
s
|
26
|
+
s.each{|x,y,val| assert_equal 0, val, "Sudoku initialise a 0 partout"}
|
13
27
|
end
|
14
28
|
|
15
29
|
def test_diagonal
|
16
|
-
s =
|
30
|
+
s = create.make_diagonal
|
17
31
|
(s.size-1).times{|i| assert_equal 1, s.get(i+1, i+1)-s.get(i, i)}
|
18
32
|
end
|
19
33
|
|
@@ -34,15 +48,16 @@ module GridTest
|
|
34
48
|
|
35
49
|
def test_clone
|
36
50
|
s1 = create
|
37
|
-
s1.set
|
51
|
+
s1.each{|x,y,v| s1.set x, y, rand(s1.size)+1}
|
38
52
|
|
39
53
|
s2 = s1.clone
|
40
|
-
|
41
|
-
|
54
|
+
s2.each do |x, y, v|
|
55
|
+
assert_equal s1.get(x,y), v, "Clonage cellule #{x},#{y}"
|
56
|
+
end
|
42
57
|
end
|
43
58
|
|
44
59
|
def test_each
|
45
|
-
s =
|
60
|
+
s = create.make_diagonal
|
46
61
|
s.size.times{|x| assert_equal x+1, s.get(x, x)}
|
47
62
|
end
|
48
63
|
|
@@ -67,25 +82,27 @@ module GridTest
|
|
67
82
|
|
68
83
|
def test_possibilities
|
69
84
|
s = create
|
70
|
-
s.set
|
85
|
+
s.set 1,0,1
|
71
86
|
s.set 0,1,2
|
72
87
|
|
73
88
|
assert s.possibilities(1, 1).include?(3)
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
assert s.valid?(1,1,4)
|
89
|
+
refute s.possibilities(1, 1).include?(1)
|
90
|
+
refute s.valid?(1,1,1), "Case non occupee, valeur impossible"
|
91
|
+
refute s.valid?(0,1,1), "Case deja occupee, valeur impossible"
|
92
|
+
assert s.valid?(1,1,4), "Case non occupee, valeur plausible"
|
93
|
+
assert s.valid?(1,1,0), "0 est toujours valide"
|
94
|
+
assert s.valid?(1,0,1), "Case deja occupee, valeur plausible"
|
78
95
|
|
79
|
-
|
80
|
-
|
81
|
-
|
96
|
+
s.possibilities(1, 1).each do |p|
|
97
|
+
assert s.valid?(1, 1, p), "Possibilite #{p} doit etre valide"
|
98
|
+
end
|
82
99
|
end
|
83
100
|
|
84
101
|
def test_sutxt
|
85
102
|
sutxt = "#{base}:"
|
86
103
|
size = base*base
|
87
104
|
size.times do |y|
|
88
|
-
size.times {|x| sutxt
|
105
|
+
size.times {|x| sutxt += " #{x+1}"}
|
89
106
|
end
|
90
107
|
sutxt += ';'
|
91
108
|
|
@@ -107,38 +124,94 @@ module GridTest
|
|
107
124
|
assert s.completable?
|
108
125
|
refute s.valid?
|
109
126
|
end
|
110
|
-
end
|
111
127
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
128
|
+
def test_count
|
129
|
+
s = create
|
130
|
+
assert_equal s.length, s.count(0)[0], "Comptage de 0 dans un sudoku vide"
|
131
|
+
|
132
|
+
s.make_diagonal
|
133
|
+
expected = {}
|
134
|
+
s.size.times{|x| expected[x+1] = 1}
|
135
|
+
assert_equal expected, s.count, "Comptage de valeurs dans un sudoku diagonal"
|
136
|
+
|
137
|
+
assert_raise(ArgumentError, "Comptage de valeur > size impossible"){s.count(s.size+1)}
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_import
|
141
|
+
s = create.make_valid
|
142
|
+
s2 = Sudoku::Sn.new(s.size)
|
143
|
+
s2.each do |x,y,v|
|
144
|
+
assert_equal s.get(x,y), v, "Importation d'une grille vers grille generique, cellules egales"
|
116
145
|
end
|
146
|
+
|
147
|
+
s3 = Sudoku::Sn.new(s.size+1)
|
148
|
+
assert_raise(Sudoku::NotCompatibleError, "Importation d'une grille vers grille de taille differente"){s3.import s}
|
117
149
|
end
|
118
|
-
|
119
|
-
|
120
|
-
|
150
|
+
end
|
151
|
+
|
152
|
+
module GeneratorTest
|
153
|
+
def test_generator_diagonal
|
154
|
+
s = create.make_diagonal
|
121
155
|
s.size.times{|x| assert_equal x+1, s.get(x, x)}
|
122
156
|
end
|
123
157
|
|
124
|
-
def
|
125
|
-
s =
|
158
|
+
def test_generator_valid
|
159
|
+
s = create.make_valid
|
126
160
|
assert s.valid?
|
127
161
|
assert s.completable?
|
128
162
|
assert s.complete?
|
129
163
|
end
|
130
164
|
end
|
131
165
|
|
132
|
-
|
166
|
+
module SolverTest
|
167
|
+
SOLVER_TIMEOUT = 10 #sec
|
168
|
+
|
169
|
+
def test_missing
|
170
|
+
s = create
|
171
|
+
s.set 0,1,1
|
172
|
+
s.set 1,0,2
|
173
|
+
|
174
|
+
assert s.missing_square(0,0).include?(3)
|
175
|
+
refute s.missing_square(0,0).include?(1)
|
176
|
+
|
177
|
+
assert s.missing_col(0).include?(2)
|
178
|
+
refute s.missing_col(0).include?(1)
|
179
|
+
|
180
|
+
assert s.missing_row(0).include?(1)
|
181
|
+
refute s.missing_row(0).include?(2)
|
182
|
+
end
|
183
|
+
|
184
|
+
def test_solve_uniq
|
185
|
+
s = create.make_valid
|
186
|
+
s.set 0,0,0
|
187
|
+
s.set 1,1,0
|
188
|
+
|
189
|
+
assert_equal 2, s.solve_uniq_possibilities!, "Solution par possibilites uniques d'un sudoku complet-2cases"
|
190
|
+
assert s.complete?
|
191
|
+
end
|
192
|
+
|
193
|
+
def test_solve_backtrack
|
194
|
+
s = create.make_valid_incomplete
|
195
|
+
t = Thread.new(s){|sudoku| sudoku.solve_backtrack!}
|
196
|
+
t.join SOLVER_TIMEOUT
|
197
|
+
assert s.complete?, "Toujours une solution en backtracking en max. #{SOLVER_TIMEOUT}s"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
module SudokuTest
|
133
202
|
include GridTest
|
203
|
+
include SolverTest
|
134
204
|
include GeneratorTest
|
205
|
+
end
|
206
|
+
|
207
|
+
class S3Test < MyTestCase
|
208
|
+
include SudokuTest
|
135
209
|
|
136
210
|
def base; 3; end
|
137
211
|
def klass; Sudoku::S3; end
|
138
|
-
def create; klass.new; end
|
139
212
|
|
140
213
|
def test_square
|
141
|
-
s =
|
214
|
+
s = create.make_diagonal
|
142
215
|
9.times do |i|
|
143
216
|
assert_equal [1+3*(i/3), 2+3*(i/3), 3+3*(i/3)], s.square(i,i)
|
144
217
|
end
|
@@ -146,14 +219,28 @@ class S3Test < Test::Unit::TestCase
|
|
146
219
|
|
147
220
|
def test_possibilities
|
148
221
|
super
|
149
|
-
s =
|
222
|
+
s = create.make_diagonal
|
150
223
|
assert_equal [1, 4, 5, 6, 7, 8, 9], s.possibilities(0, 0).sort
|
151
224
|
end
|
225
|
+
|
226
|
+
#Tests pour l'implementation en C de valid_cell?
|
227
|
+
def test_valid_cimpl
|
228
|
+
s = klass.new
|
229
|
+
s.set 0, 7, 1
|
230
|
+
s.set 7, 0, 2
|
231
|
+
s.set 1, 1, 3
|
232
|
+
|
233
|
+
refute s.valid?(0, 0, 1), "Colonne"
|
234
|
+
refute s.valid?(0, 0, 2), "Ligne"
|
235
|
+
refute s.valid?(0, 0, 3), "Carre"
|
236
|
+
assert s.valid?(0, 0, 4)
|
237
|
+
|
238
|
+
assert_raise(ArgumentError){s.valid?(9, 2, 3)}
|
239
|
+
end
|
152
240
|
end
|
153
241
|
|
154
|
-
class S4_15Test <
|
155
|
-
include
|
156
|
-
include GeneratorTest
|
242
|
+
class S4_15Test < MyTestCase
|
243
|
+
include SudokuTest
|
157
244
|
|
158
245
|
def base; 4; end
|
159
246
|
def klass; Sudoku::S4_15; end
|
@@ -163,19 +250,35 @@ class S4_15Test < Test::Unit::TestCase
|
|
163
250
|
end
|
164
251
|
end
|
165
252
|
|
166
|
-
class SnTest <
|
167
|
-
include
|
168
|
-
include GeneratorTest
|
253
|
+
class SnTest < MyTestCase
|
254
|
+
include SudokuTest
|
169
255
|
|
170
256
|
def base; 2; end
|
171
257
|
def klass; Sudoku::Sn; end
|
172
258
|
end
|
173
259
|
|
174
|
-
class
|
260
|
+
class GlobalTest < Test::Unit::TestCase
|
175
261
|
def test_autoclass
|
176
|
-
|
177
|
-
|
178
|
-
|
262
|
+
{Sudoku::S3 => 3, Sudoku::S4_15 => 4, Sudoku::Sn => 2}.each do |klass, base|
|
263
|
+
assert_equal klass, Sudoku[base], "Sudoku[#{base}] => #{klass}"
|
264
|
+
end
|
265
|
+
|
266
|
+
Sudoku[2..7] = GridTest
|
267
|
+
assert_equal GridTest, Sudoku[6], "Definition d'autoclasses persos"
|
268
|
+
end
|
269
|
+
|
270
|
+
def test_speed
|
271
|
+
t = [Time.now]
|
272
|
+
klasses = [Sudoku::S3, Sudoku::S4_15, Sudoku::Sn]
|
273
|
+
|
274
|
+
klasses.each do |k|
|
275
|
+
s = k.new 3
|
276
|
+
1000.times{|i| s.each{|x,y,v| s.valid? x,y,9}}
|
277
|
+
t << Time.now
|
278
|
+
end
|
279
|
+
|
280
|
+
2.times do |i|
|
281
|
+
assert (t[i+1] - t[i])<(t[i+2] - t[i+1]), "#{klasses[i]} plus rapide que #{klasses[i+1]}"
|
179
282
|
end
|
180
283
|
end
|
181
284
|
end
|