-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcrossword.rb
410 lines (360 loc) · 12.8 KB
/
crossword.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# Generate Crossword and Findword puzzles. Ruby version of Python Crossword generator code.
# Find the original on http://bryanhelmig.com/python-crossword-puzzle-generator/
require 'date'
class Crossword
attr_accessor :cols, :rows, :empty, :maxloops, :available_words, :grid, :current_word_list
def initialize(cols, rows, empty = '-', maxloops = 2000, available_words=[])
@cols = cols
@rows = rows
@empty = empty
@maxloops = maxloops
@available_words = available_words
randomize_word_list
@current_word_list = []
@debug = 0
clear_grid
end
def clear_grid() # initialize grid and fill with empty character
@grid = []
@rows.times do
ea_row = []
@cols.times do
ea_row << @empty
end
@grid << ea_row
end
end
def randomize_word_list() # also resets words and sorts by length
temp_list = []
@available_words.each do |word|
if word.instance_of?(Word)
temp_list << Word.new(word.word, word.clue)
else
temp_list << Word.new(word[0], word[1])
end
end
temp_list = temp_list.shuffle
temp_list = temp_list.sort_by(&:length).reverse
@available_words = temp_list
end
def compute_crossword(time_permitted = 1.00, spins=2)
time_permitted = time_permitted.to_f
count = 0
copy = Crossword.new(@cols, @rows, @empty, @maxloops, @available_words)
start_full = DateTime.now.strftime('%s').to_f
while ((DateTime.now.strftime('%s').to_f - start_full) < time_permitted || count == 0) do# only run for x seconds
@debug += 1
copy.current_word_list = []
copy.clear_grid()
copy.randomize_word_list()
x = 0
while x < spins do # spins; 2 seems to be plenty
copy.available_words.each do |word|
unless copy.current_word_list.include?(word) # if word not in copy.current_word_list:
copy.fit_and_add(word)
end
end
x +=1
end
#print copy.solution()
#print len(copy.current_word_list), len(@current_word_list), @debug
# buffer the best crossword by comparing placed words
if copy.current_word_list.length > @current_word_list.length
@current_word_list = copy.current_word_list
@grid = copy.grid
end
count +=1
end
return
end
def suggest_coord(word)
count = 0
coordlist = []
glc = -1
word.word.each_char do |given_letter|# cycle through letters in word
glc += 1
rowc = 0
@grid.each do |row| # cycle through rows
rowc += 1
colc = 0
row.each do |cell|# cycle through letters in rows
colc += 1
if given_letter == cell # check match letter in word to letters in row
begin # suggest vertical placement
if rowc - glc > 0 # make sure we're not suggesting a starting point off the grid
if ((rowc - glc) + word.length) <= @rows # make sure word doesn't go off of grid
coordlist << [colc, rowc - glc, 1, colc + (rowc - glc), 0]
end
end
rescue Exception => e
#nothing
end
begin # suggest horizontal placement
if colc - glc > 0 # make sure we're not suggesting a starting point off the grid
if ((colc - glc) + word.length) <= @cols # make sure word doesn't go off of grid
coordlist << [colc - glc, rowc, 0, rowc + (colc - glc), 0]
end
end
rescue Exception => e
#nothing
end
end
end
end
end
# example: coordlist[0] = [col, row, vertical, col + row, score]
#print word.word
#print coordlist
new_coordlist = sort_coordlist(coordlist, word)
#print new_coordlist
return new_coordlist
end
def sort_coordlist(coordlist, word) # give each coordinate a score, then sort
new_coordlist = []
coordlist.each do |coord|
col = coord[0]
row = coord[1]
vertical = coord[2]
coord[4] = check_fit_score(col, row, vertical, word) # checking scores
if coord[4] != 0 # 0 scores are filtered
new_coordlist << coord
end
end
new_coordlist = new_coordlist.shuffle
new_coordlist = new_coordlist.sort_by { |i| i[4] }
new_coordlist = new_coordlist.reverse # new_coordlist.sort(key=lambda i: i[4], reverse=True) # put the best scores first
return new_coordlist
end
def fit_and_add(word) # doesn't really check fit except for the first word; otherwise just adds if score is good
fit = false
count = 0
coordlist = suggest_coord(word)
while (fit == false && count < @maxloops) do # while not fit and count < @maxloops: OPTIMIZE
if current_word_list.length == 0 # this is the first word: the seed
# top left seed of longest word yields best results (maybe override)
vertical = [0,1].sample
col = 1
row = 1
if check_fit_score(col, row, vertical, word)
fit = true
set_word(col, row, vertical, word, true)
end
else # a subsquent words have scores calculated
begin
col = coordlist[count][0]
row = coordlist[count][1]
vertical = coordlist[count][2]
rescue Exception => e #IndexError: return # no more cordinates, stop trying to fit OPTIMIZE
return
end
if coordlist[count][4] != 0 # already filtered these out, but double check
fit = true
set_word(col, row, vertical, word, true)
end
end
count += 1
end
return true
end
def check_fit_score(col, row, vertical, word)
#And return score (0 signifies no fit). 1 means a fit, 2+ means a cross.
#The more crosses the better.
if col < 1 or row < 1
return 0
end
count = 1 # give score a standard value of 1, will override with 0 if collisions detected
score = 1
word.word.each_char do |letter|
begin
active_cell = get_cell(col, row)
rescue Exception => e # IndexError:
return 0
end
if active_cell == @empty || active_cell == letter
# NOTHING
else
return 0
end
if active_cell == letter
score += 1
end
if vertical != 0
# check surroundings
if active_cell != letter # don't check surroundings if cross point
unless check_if_cell_clear(col+1, row) # check right cell
return 0
end
unless check_if_cell_clear(col-1, row) # check left cell
return 0
end
end
if count == 1 # check top cell only on first letter
unless check_if_cell_clear(col, row-1)
return 0
end
end
if count == word.word.length # check bottom cell only on last letter
unless check_if_cell_clear(col, row+1)
return 0
end
end
else # else horizontal
# check surroundings
if active_cell != letter # don't check surroundings if cross point
unless check_if_cell_clear(col, row-1) # check top cell
return 0
end
unless check_if_cell_clear(col, row+1) # check bottom cell
return 0
end
end
if count == 1 # check left cell only on first letter
unless check_if_cell_clear(col-1, row)
return 0
end
end
if count == word.word.length # check right cell only on last letter
unless check_if_cell_clear(col+1, row)
return 0
end
end
end
if vertical != 0 # progress to next letter and position
row += 1
else # else horizontal
col += 1
end
count += 1
end
return score
end
def set_word(col, row, vertical, word, force=false) # also adds word to word list
if force
word.col = col
word.row = row
word.vertical = vertical
@current_word_list << word
word.word.each_char do |letter|
set_cell(col, row, letter)
if vertical != 0
row += 1
else
col += 1
end
end
end
return
end
def set_cell(col, row, value)
@grid[row-1][col-1] = value
end
def get_cell(col, row)
return @grid[row-1][col-1]
end
def check_if_cell_clear(col, row)
begin
cell = get_cell(col, row)
if cell == @empty
return true
end
rescue Exception => e # IndexError:
# pass
end
return false
end
def solution() # return solution grid
outStr = ""
@rows.times do |r| #for r in range(@rows):
@grid[r].each do |c|
outStr += "#{c} "
end
outStr += "\n"
end
return outStr
end
def word_find() # return solution grid OPTIMIZE
outStr = ""
@rows.times do |r|
@grid[r].each do |c|
if c == @empty
ran_char = ('a'..'z').to_a.sample
outStr += "#{ran_char} "
else
outStr += "#{c} "
end
end
outStr += "\n"
end
return outStr
end
def order_number_words() # orders words and applies numbering system to them
@current_word_list = @current_word_list.sort_by { |i| i.col + i.row}
count = 1
icount = 1
@current_word_list.each do |word|
word.number = count
if icount < @current_word_list.length
if word.col == @current_word_list[icount].col && word.row == @current_word_list[icount].row
# NOTHING
else
count += 1
end
end
icount += 1
end
end
def display(order=true) # return (and order/number wordlist) the grid minus the words adding the numbers
outStr = ""
if order
order_number_words
end
copy = Marshal.load(Marshal.dump(self))
copy.current_word_list.each do |word|
copy.set_cell(word.col, word.row, word.number)
end
copy.rows.times do |r|
copy.grid[r].each do |c|
outStr += "#{c} "
end
outStr += "\n"
end
outStr = outStr.gsub(/[a-z]/, ' ')
return outStr
end
def word_bank()
outStr = ''
temp_list = @current_word_list.clone # duplicate(@current_word_list)
temp_list.shuffle
temp_list.each do |word|
outStr += "#{word.word}\n" # '%s\n' % word.word
end
return outStr
end
def legend() # must order first
outStr = ''
@current_word_list.each do |word|
outStr += "#{word.number}. (#{word.col}, #{word.row}) #{word.down_across}: #{word.clue}\n"
end
return outStr
end
end
class Word
attr_accessor :word, :clue, :length, :row, :col, :vertical, :number
def initialize(word=nil, clue=nil)
@word = word.downcase.gsub(/\s/,'')
@clue = clue
@length = @word.length
# the below are set when placed on board
@row = nil
@col = nil
@vertical = nil
@number = nil
end
def down_across() # return down or across
if @vertical != 0
return 'down'
else
return 'across'
end
end
end