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
|
############################################################################
#
# File: makepuzz.icn
#
# Subject: Program to make find-the-word puzzle
#
# Author: Richard L. Goerwitz
#
# Date: May 2, 2001
#
###########################################################################
#
# This file is in the public domain.
#
############################################################################
#
# Version: 1.19
#
###########################################################################
#
# This program doesn't do anything fancy. It simply takes a list
# of words, and constructs out of them one of those square
# find-the-word puzzles that some people like to bend their minds
# over. Usage is:
#
# makepuzz [-f input-file] [-o output-file] [-h puzzle-height]
# -w puzzle-width] [-t how-many-seconds-to-keep-trying]
# [-r maximum-number-of-rejects] [-s] [-d]
#
# where input-file is a file containing words, one to a line
# (defaults to &input), and output-file is the file you would like the
# puzzle written to (defaults to &output). Puzzle-height and width
# are the basic dimensions you want to try to fit your word game into
# (default 20x20). If the -s argument is present, makepuzz will
# scramble its output, by putting random letters in all the blank
# spaces. The -t tells the computer when to give up, and construct
# the puzzle (letting you know if any words didn't make it in).
# Defaults to 60 (i.e. one minute). The -r argument tells makepuzz to
# run until it arrives at a solution with number-of-rejects or less
# un-inserted words. -d turns on certain diagnostic messages.
#
# Most of these options can safely be ignored. Just type
# something like "makepuzz -f wordlist," where wordlist is a file
# containing about sixty words, one word to a line. Out will pop a
# "word-find" puzzle. Once you get the hang of what is going on,
# try out the various options.
#
# The algorithm used here is a combination of random insertions
# and mindless, brute-force iterations through possible insertion
# points and insertion directions. If you don't like makepuzz's per-
# formance on one run, run it again. If your puzzle is large, try
# increasing the timeout value (see -t above).
#
############################################################################
#
# Links: options, random, colmize
#
############################################################################
link options
link random
link colmize
global height, width, _debug_
procedure main(a)
local usage, opttbl, inputfile, outputfile, maxrejects, puzzle,
wordlist, rejects, master_list, word, timeout, x, y, l_puzzle,
l_wordlist, l_rejects, no_ltrs, l_no_ltrs, try, first_time
# Filename is the only mandatory argument; they can come in any order.
usage := "makepuzz [-f infile] [-o outfile] [-h height] [-w width] _
[-t secs] [-r rejects] [-s]"
# Set up puzzle height and width (default 20x20); set up defaults
# such as the input & output files, time to spend, target reject
# count, etc.
opttbl := options(a, "w+h+f:o:t+sr+d") # stop(usage)
width := \opttbl["w"] | 20
height := \opttbl["h"] | 20
timeout := &time + (1000 * (\opttbl["t"] | 60))
inputfile := open(\opttbl["f"], "r") | &input
outputfile := open(\opttbl["o"], "w") | &output
maxrejects := \opttbl["r"] | 0
_debug_ := \opttbl["d"] & try := 0
first_time := 1
# Set random number seed.
randomize()
# Read, check, and sort word list hardest to easiest.
master_list := list()
every word := "" ~== trim(map(!inputfile)) do {
upto(~(&lcase++&ucase), word) &
stop("makepuzz: non-letter found in ", word)
write(&errout, "makepuzz: warning, ",3 > *word,
"-letter word (", word, ")")
put(master_list, word)
}
master_list := sort_words(master_list)
if \_debug_ then write(&errout, "makepuzz: thinking...")
# Now, try to insert the words in the master list into a puzzle.
# Stop when the timeout limit is reached (see -t above).
until &time > timeout & /first_time do {
first_time := &null
wordlist := copy(master_list); rejects := list()
puzzle := list(height); every !puzzle := list(width)
blind_luck_insert(puzzle, wordlist, rejects)
brute_force_insert(puzzle, wordlist, rejects, timeout)
# Count the number of letters left over.
no_ltrs := 0; every no_ltrs +:= *(!wordlist | !rejects)
l_no_ltrs := 0; every l_no_ltrs +:= *(!\l_wordlist | !\l_rejects)
# If our last best try at making a puzzle was worse...
if /l_puzzle |
(*\l_wordlist + *l_rejects) > (*wordlist + *rejects) |
((*\l_wordlist + *l_rejects) = (*wordlist + *rejects) &
l_no_ltrs > no_ltrs)
then {
# ...then save the current (better) one.
l_puzzle := puzzle
l_wordlist := wordlist
l_rejects := rejects
}
# Tell the user how we're doing.
if \_debug_ then
write(&errout, "makepuzz: try number ", try +:= 1, "; ",
*wordlist + *rejects, " rejects")
# See the -r argument above. Stop if we get to a number of
# rejects deemed acceptable to the user.
if (*\l_wordlist + *l_rejects) <= maxrejects then break
}
# Signal to user that we're done, and set puzzle, wordlist, and
# rejects to their best values in this run of makepuzz.
write(&errout, "makepuzz: done")
puzzle := \l_puzzle
wordlist := \l_wordlist
rejects := \l_rejects
# Print out original word list, and list of words that didn't make
# it into the puzzle.
write(outputfile, "Original word list (sorted hardest-to-easiest): \n")
every write(outputfile, colmize(master_list))
write(outputfile, "")
if *rejects + *wordlist > 0 then {
write(outputfile, "Couldn't insert the following words: \n")
every write(outputfile, colmize(wordlist ||| rejects))
write(outputfile, "")
}
# Scramble (i.e. put in letters for remaining spaces) if the user
# put -s on the command line.
if \opttbl["s"] then {
every y := !puzzle do
every x := 1 to *y do
/y[x] := ?&ucase
# Print out puzzle structure (answers in lowercase).
every y := !puzzle do {
every x := !y do
writes(outputfile, \x | " ", " ")
write(outputfile, "")
}
write(outputfile, "")
}
# Print out puzzle structure, all lowercase.
every y := !puzzle do {
every x := !y do
writes(outputfile, map(\x) | " ", " ")
write(outputfile, "")
}
# Exit with default OK status for this system.
every close(inputfile | outputfile)
exit()
end
procedure sort_words(wordlist)
local t, t2, word, sum, l
# Obtain a rough character count.
t := table(0)
every t[!!wordlist] +:= 1
t2 := table()
# Obtain weighted values for each word, essentially giving longer
# words and words with uncommon letters the highest values. Later
# we'll reverse the order (-> hardest-to-easiest), and return a list.
every word := !wordlist do {
"" == word & next
sum := 0
every sum +:= t[!word]
insert(t2, word, (sum / *word) - (2 * *word))
}
t2 := sort(t2, 4)
l := list()
# Put the hardest words first. These will get laid down when the
# puzzle is relatively empty. Save the small, easy words for last.
every put(l, t2[1 to *t2-1 by 2])
return l
end
procedure blind_luck_insert(puzzle, wordlist, rejects)
local s, s2, s3, begy, begx, y, x, diry, dirx, diry2, dirx2, i
# global height, width
# Try using blind luck to make as many insertions as possible.
while s := get(wordlist) do {
# First try squares with letters already on them, but don't
# try every direction yet (we're relying on luck just now).
# Start at a random spot in the puzzle, and wrap around.
begy := ?height; begx := ?width
every y := (begy to height) | (1 to begy - 1) do {
every x := (begx to width) | (1 to begx - 1) do {
every i := find(\puzzle[y][x], s) do {
diry := ?3; dirx := ?3
s2 := s[i:0]
diry2 := 4 > (diry + 2) | 0 < (diry - 2) | 2
dirx2 := 4 > (dirx + 2) | 0 < (dirx - 2) | 2
s3 := reverse(s[1:i+1])
if insert_word(puzzle, s2, diry, dirx, y, x) &
insert_word(puzzle, s3, diry2, dirx2, y, x)
then break { break break next }
}
}
}
# If the above didn't work, give up on spaces with characters
# in them; use blank squares as well.
every 1 to 512 do
if insert_word(puzzle, s, ?3, ?3, ?height, ?width) then
break next
# If this word doesn't submit to easy insertion, save it for
# later.
put(rejects, s)
}
# Nothing useful to return (puzzle, wordlist, and rejects objects
# are themselves modified; not copies of them).
return
end
procedure brute_force_insert(puzzle, wordlist, rejects, timeout)
local s, start, dirs, begy, begx, y, x
# Use brute force on the remaining forms.
if *rejects > 0 then {
wordlist |||:= rejects; rejects := []
while s := pop(wordlist) do {
start := ?3; dirs := ""
every dirs ||:= ((start to 3) | (1 to start-1))
begy := ?height; begx := ?width
every y := (begy to height) | (1 to begy - 1) do {
if &time > timeout then fail
every x := (begx to width) | (1 to begx - 1) do {
if insert_word(puzzle, s, !dirs, !dirs, y, x) then
break { break next }
}
}
# If we can't find a place for s, put it in the rejects list.
put(rejects, s)
}
}
# Nothing useful to return (puzzle, wordlist, and rejects objects
# are themselves modified; not copies of them).
return
end
procedure insert_word(puzzle, s, ydir, xdir, y, x)
local incry, incrx, firstchar
# If s is zero length, we've matched it in it's entirety!
if *s = 0 then {
return
} else {
# Make sure there's enough space in the puzzle in the direction
# we're headed.
case ydir of {
"3": if (height - y) < (*s - 1) then fail
"1": if y < (*s - 1) then fail
}
case xdir of {
"3": if (width - x) < (*s - 1) then fail
"1": if x < (*s - 1) then fail
}
# Check to be sure everything's in range, and that both the x and
# y increments aren't zero (in which case, we aren't headed in any
# direction at all...).
incry := (ydir - 2); incrx := (xdir - 2)
if incry = 0 & incrx = 0 then fail
height >= y >= 1 | fail
width >= x >= 1 | fail
# Try laying the first char in s down at puzzle[y][x]. If it
# works, head off in some direction, and try laying down the rest
# of s along that vector. If at any point we fail, we must
# reverse the assignment (<- below).
firstchar := !s
((/puzzle[y][x] <- firstchar) | (\puzzle[y][x] == firstchar)) &
insert_word(puzzle, s[2:0], ydir, xdir, y + incry, x + incrx) &
suspend
fail
}
end
|