retoo-rudoku 0.1.1
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.rdoc +7 -0
- data/lib/rudoku.rb +351 -0
- metadata +54 -0
data/README.rdoc
ADDED
data/lib/rudoku.rb
ADDED
@@ -0,0 +1,351 @@
|
|
1
|
+
# Copyright (c) 2007-2009 by Reto Schüttel <reto (ät) schuettel (dut) ch>
|
2
|
+
|
3
|
+
# Please contact me if you like this released under a OSS license!
|
4
|
+
|
5
|
+
class Rudoku
|
6
|
+
NRS = { 1 => true, 2 => true, 3 => true,
|
7
|
+
4 => true, 5 => true, 6 => true,
|
8
|
+
7 => true, 8 => true, 9 => true, }
|
9
|
+
|
10
|
+
class Board
|
11
|
+
attr_reader :fields, :blocks, :rows, :cols
|
12
|
+
|
13
|
+
def initialize(board)
|
14
|
+
@fields = []
|
15
|
+
@board = []
|
16
|
+
@rows = []
|
17
|
+
@cols = []
|
18
|
+
@blocks = []
|
19
|
+
@missing = []
|
20
|
+
@stats = {
|
21
|
+
:missing => 0,
|
22
|
+
:solved_by_only_one_possible => 0,
|
23
|
+
:solved_by_no_other_possible => 0,
|
24
|
+
:solved_by_presolve => 0
|
25
|
+
}
|
26
|
+
|
27
|
+
# initialize rows, columns & blocks
|
28
|
+
0.upto(8) do |i|
|
29
|
+
@rows[i] = Row.new(self, i)
|
30
|
+
@cols[i] = Col.new(self, i)
|
31
|
+
@blocks[i] = Block.new(self, i)
|
32
|
+
end
|
33
|
+
|
34
|
+
i = 0
|
35
|
+
board.each_with_index do |row, y|
|
36
|
+
@board[y] = []
|
37
|
+
|
38
|
+
row.each_with_index do |value, x|
|
39
|
+
f = Field.new(self, i, x, y)
|
40
|
+
f.value = value
|
41
|
+
|
42
|
+
@board[y][x] = f
|
43
|
+
@fields << f
|
44
|
+
@missing << f if f.missing?
|
45
|
+
i += 1
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# initialize the helper constructs
|
50
|
+
@blocks.each do |block|
|
51
|
+
block.initialize_area()
|
52
|
+
end
|
53
|
+
|
54
|
+
@stats[:missing] = @missing.length
|
55
|
+
|
56
|
+
@counter = 0
|
57
|
+
|
58
|
+
raise "Invalid field" if not valid_field?
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def pre_solve
|
63
|
+
begin
|
64
|
+
#puts "try"
|
65
|
+
changed = @missing.reject! do |f|
|
66
|
+
# create a list of all fields which are wmissing
|
67
|
+
# in the same block, not including t the current field f
|
68
|
+
nrs = f.available_nrs
|
69
|
+
|
70
|
+
|
71
|
+
#puts "#{f} hat #{nrs.length} möglichkeiten: #{nrs.join(" ")}" if nrs.length == 2
|
72
|
+
#f.mark if nrs.length ==2
|
73
|
+
if nrs.length == 1
|
74
|
+
@stats[:solved_by_only_one_possible] += 1
|
75
|
+
#f.mark
|
76
|
+
f.value = nrs.first
|
77
|
+
true
|
78
|
+
else
|
79
|
+
# use the scanning method for possible other solutions
|
80
|
+
# returns false or true
|
81
|
+
|
82
|
+
if v = f.nr_only_possible_in_this_field
|
83
|
+
# f.mark
|
84
|
+
@stats[:solved_by_no_other_possible] += 1
|
85
|
+
f.value = v
|
86
|
+
#f.mark
|
87
|
+
#return
|
88
|
+
true
|
89
|
+
else
|
90
|
+
false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end while changed
|
95
|
+
|
96
|
+
@stats[:solved_by_presolve] =
|
97
|
+
@stats[:solved_by_no_other_possible] +
|
98
|
+
@stats[:solved_by_only_one_possible]
|
99
|
+
end
|
100
|
+
|
101
|
+
# returns false if there's no possible next move, else return the valid path
|
102
|
+
def solve(level = 0)
|
103
|
+
fm = @missing.shift
|
104
|
+
|
105
|
+
if fm.nil?
|
106
|
+
return true
|
107
|
+
end
|
108
|
+
|
109
|
+
available_nrs = fm.available_nrs
|
110
|
+
|
111
|
+
unless available_nrs.empty?
|
112
|
+
# try all the available nrs on this field
|
113
|
+
available_nrs.each do |nr|
|
114
|
+
|
115
|
+
raise "nr is nil??" if nr.nil?
|
116
|
+
# set it
|
117
|
+
fm.value = nr
|
118
|
+
|
119
|
+
# and try to fill the next field
|
120
|
+
result = solve(level + 1)
|
121
|
+
|
122
|
+
# return true if we found a solution
|
123
|
+
return true if result
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# seems like we are in a 'sackgasse', reset the value and
|
128
|
+
# go back to the next stepp
|
129
|
+
fm.value = nil
|
130
|
+
@missing.unshift(fm)
|
131
|
+
|
132
|
+
return false
|
133
|
+
end
|
134
|
+
|
135
|
+
def print_field(p = [])
|
136
|
+
p = [p] unless p.kind_of?(Array)
|
137
|
+
|
138
|
+
@board.each_with_index do |row, y|
|
139
|
+
#row.map{|f| f.value}.map{|f| f || "_"}.each_with_index do |f, x|
|
140
|
+
row.each_with_index do |f, x|
|
141
|
+
t = ( f.value ? f.value.to_s : "_" ) + ( f.marked? ? "<" : " " )
|
142
|
+
|
143
|
+
print "#{t} "
|
144
|
+
|
145
|
+
#if p.any?{|o| x == o.x && y == o.y}
|
146
|
+
# print "#{f}<"
|
147
|
+
#else
|
148
|
+
# print "#{f} "
|
149
|
+
#end
|
150
|
+
print " " if x % 3 == 2
|
151
|
+
end
|
152
|
+
puts
|
153
|
+
puts if y % 3 == 2
|
154
|
+
end
|
155
|
+
|
156
|
+
#puts "Lösung"
|
157
|
+
#0.upto(8) do |i|
|
158
|
+
# print "#{@board[i][i].value} "
|
159
|
+
#end
|
160
|
+
#puts
|
161
|
+
|
162
|
+
#puts
|
163
|
+
puts "Stats"
|
164
|
+
@stats.each do |key, value|
|
165
|
+
puts "#{key}: #{value}"
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
def valid_field?
|
171
|
+
true
|
172
|
+
end
|
173
|
+
|
174
|
+
def get(x, y)
|
175
|
+
@board[y][x]
|
176
|
+
end
|
177
|
+
|
178
|
+
def get_row(y)
|
179
|
+
@rows[y]
|
180
|
+
end
|
181
|
+
|
182
|
+
def get_col(x)
|
183
|
+
@cols[x]
|
184
|
+
end
|
185
|
+
|
186
|
+
def get_block(x, y)
|
187
|
+
block_nr = x / 3 + y/3*3
|
188
|
+
@blocks[block_nr] or raise "Unitialized block at #{x}/#{y} block_nr #{block_nr}"
|
189
|
+
end
|
190
|
+
|
191
|
+
def find_first_missing
|
192
|
+
@fields.find{|f| f.missing?}
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
class Field
|
197
|
+
attr_reader :index, :x, :y, :value, :block
|
198
|
+
|
199
|
+
def initialize(board, i, x, y)
|
200
|
+
@board = board
|
201
|
+
@index = i
|
202
|
+
@x = x
|
203
|
+
@y = y
|
204
|
+
@marked = false
|
205
|
+
|
206
|
+
@row = @board.get_row(y) or raise
|
207
|
+
@col = @board.get_col(x) or raise
|
208
|
+
@block = @board.get_block(x, y) or raise
|
209
|
+
end
|
210
|
+
|
211
|
+
def mark
|
212
|
+
@marked = true
|
213
|
+
end
|
214
|
+
|
215
|
+
def marked?
|
216
|
+
@marked
|
217
|
+
end
|
218
|
+
|
219
|
+
def value=(v)
|
220
|
+
unless value.nil?
|
221
|
+
add(value)
|
222
|
+
end
|
223
|
+
|
224
|
+
unless v.nil?
|
225
|
+
remove(v)
|
226
|
+
end
|
227
|
+
@value = v
|
228
|
+
end
|
229
|
+
|
230
|
+
def remove(v)
|
231
|
+
@block.remove(v)
|
232
|
+
@row.remove(v)
|
233
|
+
@col.remove(v)
|
234
|
+
end
|
235
|
+
|
236
|
+
def add(v)
|
237
|
+
@block.add(v)
|
238
|
+
@row.add(v)
|
239
|
+
@col.add(v)
|
240
|
+
end
|
241
|
+
|
242
|
+
def missing?
|
243
|
+
@value.nil?
|
244
|
+
end
|
245
|
+
|
246
|
+
def available_nrs
|
247
|
+
@row.available_nrs & @col.available_nrs & @block.available_nrs
|
248
|
+
end
|
249
|
+
|
250
|
+
def to_s
|
251
|
+
x.to_s + ":" + y.to_s
|
252
|
+
end
|
253
|
+
|
254
|
+
def nr_only_possible_in_this_field
|
255
|
+
neighbours = block.missing_fields.reject{|m| m == self}
|
256
|
+
|
257
|
+
available_nrs.each do |nr|
|
258
|
+
# iterate over all neighbours and check if theres a Nr (nr)
|
259
|
+
# which can't be anywere else
|
260
|
+
if neighbours.all?{|n| not n.available_nrs.include?(nr) }
|
261
|
+
# okay, nr isn't possible in all the other missing
|
262
|
+
# fields in the neighbourhood, that means it can only be
|
263
|
+
# on f
|
264
|
+
return nr
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
nil
|
269
|
+
end
|
270
|
+
|
271
|
+
end
|
272
|
+
|
273
|
+
class Area
|
274
|
+
attr_reader :board
|
275
|
+
|
276
|
+
def initialize(b, coord)
|
277
|
+
@available_nrs = NRS.dup
|
278
|
+
@board = b or raise "b not defined"
|
279
|
+
end
|
280
|
+
|
281
|
+
def available_nrs
|
282
|
+
@available_nrs.keys
|
283
|
+
end
|
284
|
+
|
285
|
+
def remove(v)
|
286
|
+
raise if v == true
|
287
|
+
|
288
|
+
@available_nrs.delete(v)
|
289
|
+
raise "bb" if v == true
|
290
|
+
end
|
291
|
+
|
292
|
+
def add(v)
|
293
|
+
@available_nrs[v] = true
|
294
|
+
end
|
295
|
+
|
296
|
+
def info(mode, v)
|
297
|
+
#puts "#{type} #{mode} #{v} available_nrs contains #{@available_nrs.join(", ")}"
|
298
|
+
end
|
299
|
+
|
300
|
+
# TODO: this could be cached/resued/
|
301
|
+
def missing_fields
|
302
|
+
fields.reject{|f| not f.missing?}
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
class Row < Area
|
307
|
+
def initialize(b, y)
|
308
|
+
@y = y
|
309
|
+
super
|
310
|
+
end
|
311
|
+
|
312
|
+
def type
|
313
|
+
"row #{@y}"
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
class Col < Area
|
318
|
+
def initialize(b, x)
|
319
|
+
@x = x
|
320
|
+
super
|
321
|
+
end
|
322
|
+
|
323
|
+
def type
|
324
|
+
"col #{@x}"
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
class Block < Area
|
329
|
+
attr_accessor :fields
|
330
|
+
|
331
|
+
def initialize(b, n)
|
332
|
+
@n = n
|
333
|
+
@x = n % 3
|
334
|
+
@y = n / 3
|
335
|
+
super
|
336
|
+
end
|
337
|
+
|
338
|
+
def initialize_area
|
339
|
+
f_x = @x * 3
|
340
|
+
f_y = @y * 3
|
341
|
+
|
342
|
+
@fields = []
|
343
|
+
|
344
|
+
f_y.upto(f_y + 2) do |y|
|
345
|
+
f_x.upto(f_x + 2) do |x|
|
346
|
+
@fields << board.get(x, y)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: retoo-rudoku
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- "Reto Sch\xC3\xBCttel"
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-04-23 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Rudoku is a simple Sudoku solver.
|
17
|
+
email: reto <hugh> at <yoh!> schuettel doto ch
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- lib/rudoku.rb
|
26
|
+
- README.rdoc
|
27
|
+
has_rdoc: true
|
28
|
+
homepage: http://github.com/retoo/rudoku
|
29
|
+
post_install_message:
|
30
|
+
rdoc_options:
|
31
|
+
- --charset=UTF-8
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: "0"
|
39
|
+
version:
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "0"
|
45
|
+
version:
|
46
|
+
requirements:
|
47
|
+
- none
|
48
|
+
rubyforge_project:
|
49
|
+
rubygems_version: 1.2.0
|
50
|
+
signing_key:
|
51
|
+
specification_version: 3
|
52
|
+
summary: Sudoku Engine.
|
53
|
+
test_files: []
|
54
|
+
|