3 # This program accepts a series of newline-separated game IDs on
4 # stdin and formats them into PostScript to be printed out. You
5 # specify using command-line options which game the IDs are for,
6 # and how many you want per page.
8 # Supported games are those which are sensibly solvable using
9 # pencil and paper: Rectangles, Pattern, Solo, Net.
11 # Command-line syntax is
13 # print.py <game-name> <format>
15 # <game-name> is one of `rect', `rectangles', `pattern', `solo',
16 # `net'. <format> is two numbers separated by an x: `2x3', for
17 # example, means two columns by three rows.
19 # The program will then read game IDs from stdin until it sees EOF,
20 # and generate as many PostScript pages on stdout as it needs.
22 # The resulting PostScript will automatically adapt itself to the
23 # size of the clip rectangle, so that the puzzles are sensibly
24 # distributed across whatever paper size you decide to use.
34 for i
in xrange(len(a
)):
48 params
, seed
= string
.split(s
, ":")
49 w
, h
= map(string
.atoi
, string
.split(params
, "x"))
52 if seed
[0] in '_'+string
.lowercase
:
53 if seed
[0] in string
.lowercase
:
54 grid
.extend([-1] * (ord(seed
[0]) - ord('a') + 1))
56 elif seed
[0] in string
.digits
:
58 while len(seed
) > 0 and seed
[0] in string
.digits
:
61 grid
.append(string
.atoi(ns
))
62 assert w
* h
== len(grid
)
63 # I'm going to arbitrarily choose to use 7pt text for the
64 # numbers, and a 14pt grid pitch.
67 # Set up coordinate system.
70 ret
.coords
= (pw
/2, pw
/2, ph
/2, ph
/2)
71 psprint(ret
, "%g %g translate" %
(-ret
.coords
[0], -ret
.coords
[2]))
72 # Draw the internal grid lines, _very_ thin (the player will
73 # need to draw over them visibly).
74 psprint(ret
, "newpath 0.01 setlinewidth")
76 psprint(ret
, "%g 0 moveto 0 %g rlineto" %
(x
* gridpitch
, h
* gridpitch
))
78 psprint(ret
, "0 %g moveto %g 0 rlineto" %
(y
* gridpitch
, w
* gridpitch
))
79 psprint(ret
, "stroke")
80 # Draw round the grid exterior, much thicker.
81 psprint(ret
, "newpath 1.5 setlinewidth")
82 psprint(ret
, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \
83 (h
* gridpitch
, w
* gridpitch
, -h
* gridpitch
))
84 psprint(ret
, "closepath stroke")
85 # And draw the numbers.
86 psprint(ret
, "/Helvetica findfont %g scalefont setfont" % textht
)
91 psprint(ret
, "%g %g (%d) ctshow" % \
92 ((x
+0.5)*gridpitch
, (h
-y
-0.5)*gridpitch
, n
))
93 return ret
.coords
, ret
.s
99 params
, seed
= string
.split(s
, ":")
101 if params
[-1:] == "w":
104 w
, h
= map(string
.atoi
, string
.split(params
, "x"))
109 n
= string
.atoi(seed
[0], 16)
111 while len(seed
) > 0 and seed
[0] in 'hv':
115 hbarriers
.append((x
, y
+1))
117 vbarriers
.append((x
+1, y
))
120 assert w
* h
== len(grid
)
121 # I'm going to arbitrarily choose a 24pt grid pitch.
126 # Set up coordinate system.
129 ret
.coords
= (pw
/2, pw
/2, ph
/2, ph
/2)
130 psprint(ret
, "%g %g translate" %
(-ret
.coords
[0], -ret
.coords
[2]))
131 # Draw the base grid lines.
132 psprint(ret
, "newpath 0.02 setlinewidth")
133 for x
in xrange(1,w
):
134 psprint(ret
, "%g 0 moveto 0 %g rlineto" %
(x
* gridpitch
, h
* gridpitch
))
135 for y
in xrange(1,h
):
136 psprint(ret
, "0 %g moveto %g 0 rlineto" %
(y
* gridpitch
, w
* gridpitch
))
137 psprint(ret
, "stroke")
138 # Draw round the grid exterior.
139 psprint(ret
, "newpath")
141 psprint(ret
, "2 setlinewidth")
142 psprint(ret
, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \
143 (h
* gridpitch
, w
* gridpitch
, -h
* gridpitch
))
144 psprint(ret
, "closepath stroke")
146 psprint(ret
, "newpath 2 setlinewidth 1 setlinecap")
147 for x
, y
in hbarriers
:
148 psprint(ret
, "%g %g moveto %g 0 rlineto" % \
149 (x
* gridpitch
, (h
- y
) * gridpitch
, gridpitch
))
150 for x
, y
in vbarriers
:
151 psprint(ret
, "%g %g moveto 0 -%g rlineto" % \
152 (x
* gridpitch
, (h
- y
) * gridpitch
, gridpitch
))
153 psprint(ret
, "stroke")
154 # And draw the symbol in each box.
155 for i
in xrange(len(grid
)):
159 # Rotate to canonical form.
164 elif v
in (3,6,9,12):
166 elif v
in (7,11,13,14):
168 # Centre on an area in the corner of the tile.
169 psprint(ret
, "gsave")
173 hoffset
= smalloffset
177 voffset
= smalloffset
178 psprint(ret
, "%g %g translate" % \
179 ((x
+ hoffset
) * gridpitch
, (h
- y
- voffset
) * gridpitch
))
180 psprint(ret
, "%g dup scale" %
(float(gridpitch
) * scale
/ 2))
181 psprint(ret
, "newpath 0.07 setlinewidth")
182 # Draw the radial lines.
183 for dx
, dy
, z
in ((1,0,1), (0,1,2), (-1,0,4), (0,-1,8)):
185 psprint(ret
, "0 0 moveto %d %d lineto" %
(dx
, dy
))
186 psprint(ret
, "stroke")
187 # Draw additional figures if desired.
189 # T-pieces have a little circular blob where the lines join.
190 psprint(ret
, "newpath 0 0 0.15 0 360 arc fill")
192 # Endpoints have a little empty square at the centre.
193 psprint(ret
, "newpath 0.35 0.35 moveto 0 -0.7 rlineto")
194 psprint(ret
, "-0.7 0 rlineto 0 0.7 rlineto closepath")
195 psprint(ret
, "gsave 1 setgray fill grestore stroke")
197 psprint(ret
, "grestore")
198 return ret
.coords
, ret
.s
200 def pattern_format(s
):
204 params
, seed
= string
.split(s
, ":")
205 w
, h
= map(string
.atoi
, string
.split(params
, "x"))
206 rowdata
= map(lambda s
: string
.split(s
, "."), string
.split(seed
, "/"))
207 assert len(rowdata
) == w
+h
208 # I'm going to arbitrarily choose to use 7pt text for the
209 # numbers, and a 14pt grid pitch.
212 gutter
= 8 # between the numbers and the grid
213 # Find the maximum number of numbers in each dimension, to
214 # determine the border size required.
215 xborder
= reduce(max, map(len, rowdata
[w
:]))
216 yborder
= reduce(max, map(len, rowdata
[:w
]))
217 # Set up coordinate system. I'm going to put the origin at the
218 # _top left_ of the grid, so that both sets of numbers get
219 # drawn the same way.
220 pw
= (w
+ xborder
) * gridpitch
+ gutter
221 ph
= (h
+ yborder
) * gridpitch
+ gutter
222 ret
.coords
= (xborder
* gridpitch
+ gutter
, w
* gridpitch
, \
223 yborder
* gridpitch
+ gutter
, h
* gridpitch
)
224 # Draw the internal grid lines. Every fifth one is thicker, as
226 psprint(ret
, "newpath 0.1 setlinewidth")
227 for x
in xrange(1,w
):
229 psprint(ret
, "%g 0 moveto 0 %g rlineto" %
(x
* gridpitch
, -h
* gridpitch
))
230 for y
in xrange(1,h
):
232 psprint(ret
, "0 %g moveto %g 0 rlineto" %
(-y
* gridpitch
, w
* gridpitch
))
233 psprint(ret
, "stroke")
234 psprint(ret
, "newpath 0.75 setlinewidth")
235 for x
in xrange(5,w
,5):
236 psprint(ret
, "%g 0 moveto 0 %g rlineto" %
(x
* gridpitch
, -h
* gridpitch
))
237 for y
in xrange(5,h
,5):
238 psprint(ret
, "0 %g moveto %g 0 rlineto" %
(-y
* gridpitch
, w
* gridpitch
))
239 psprint(ret
, "stroke")
240 # Draw round the grid exterior.
241 psprint(ret
, "newpath 1.5 setlinewidth")
242 psprint(ret
, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \
243 (-h
* gridpitch
, w
* gridpitch
, h
* gridpitch
))
244 psprint(ret
, "closepath stroke")
245 # And draw the numbers.
246 psprint(ret
, "/Helvetica findfont %g scalefont setfont" % textht
)
250 xo
= (i
+ 0.5) * gridpitch
251 yo
= (gutter
+ 0.5 * gridpitch
)
253 xo
= -(gutter
+ 0.5 * gridpitch
)
254 yo
= ((i
-w
) + 0.5) * -gridpitch
255 for j
in range(len(ns
)-1, -1, -1):
256 psprint(ret
, "%g %g (%s) ctshow" %
(xo
, yo
, ns
[j
]))
261 return ret
.coords
, ret
.s
267 params
, seed
= string
.split(s
, ":")
268 c
, r
= map(string
.atoi
, string
.split(params
, "x"))
272 if seed
[0] in '_'+string
.lowercase
:
273 if seed
[0] in string
.lowercase
:
274 grid
.extend([-1] * (ord(seed
[0]) - ord('a') + 1))
276 elif seed
[0] in string
.digits
:
278 while len(seed
) > 0 and seed
[0] in string
.digits
:
281 grid
.append(string
.atoi(ns
))
282 assert cr
* cr
== len(grid
)
283 # I'm going to arbitrarily choose to use 9pt text for the
284 # numbers, and a 16pt grid pitch.
287 # Set up coordinate system.
288 pw
= ph
= gridpitch
* cr
289 ret
.coords
= (pw
/2, pw
/2, ph
/2, ph
/2)
290 psprint(ret
, "%g %g translate" %
(-ret
.coords
[0], -ret
.coords
[2]))
291 # Draw the thin internal grid lines.
292 psprint(ret
, "newpath 0.1 setlinewidth")
293 for x
in xrange(1,cr
):
295 psprint(ret
, "%g 0 moveto 0 %g rlineto" %
(x
* gridpitch
, cr
* gridpitch
))
296 for y
in xrange(1,cr
):
298 psprint(ret
, "0 %g moveto %g 0 rlineto" %
(y
* gridpitch
, cr
* gridpitch
))
299 psprint(ret
, "stroke")
300 # Draw the thicker internal grid lines.
301 psprint(ret
, "newpath 1 setlinewidth")
302 for x
in xrange(r
,cr
,r
):
303 psprint(ret
, "%g 0 moveto 0 %g rlineto" %
(x
* gridpitch
, cr
* gridpitch
))
304 for y
in xrange(c
,cr
,c
):
305 psprint(ret
, "0 %g moveto %g 0 rlineto" %
(y
* gridpitch
, cr
* gridpitch
))
306 psprint(ret
, "stroke")
307 # Draw round the grid exterior, thicker still.
308 psprint(ret
, "newpath 1.5 setlinewidth")
309 psprint(ret
, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \
310 (cr
* gridpitch
, cr
* gridpitch
, -cr
* gridpitch
))
311 psprint(ret
, "closepath stroke")
312 # And draw the numbers.
313 psprint(ret
, "/Helvetica findfont %g scalefont setfont" % textht
)
319 s
= chr(ord('a') + n
- 10)
321 s
= chr(ord('0') + n
)
322 psprint(ret
, "%g %g (%s) ctshow" % \
323 ((x
+0.5)*gridpitch
, (cr
-y
-0.5)*gridpitch
, s
))
324 return ret
.coords
, ret
.s
329 "rectangles": rect_format
,
330 "pattern": pattern_format
,
334 if len(sys
.argv
) < 3:
335 sys
.stderr
.write("print.py: expected two arguments (game and format)\n")
338 formatter
= formatters
.get(sys
.argv
[1], None)
339 if formatter
== None:
340 sys
.stderr
.write("print.py: unrecognised game name `%s'\n" % sys
.argv
[1])
344 format
= map(string
.atoi
, string
.split(sys
.argv
[2], "x"))
345 except ValueError, e
:
348 sys
.stderr
.write("print.py: expected format such as `2x3' as second" \
353 ppp
= xx
* yy
# puzzles per page
357 s
= sys
.stdin
.readline()
359 if s
[-1:] == "\n": s
= s
[:-1]
362 pages
= int((len(ids
) + ppp
- 1) / ppp
)
364 # Output initial DSC stuff.
365 print "%!PS-Adobe-3.0"
366 print "%%Creator: print.py from Simon Tatham's Puzzle Collection"
367 print "%%DocumentData: Clean7Bit"
368 print "%%LanguageLevel: 1"
369 print "%%Pages:", pages
370 print "%%DocumentNeededResources:"
371 print "%%+ font Helvetica"
372 print "%%DocumentSuppliedResources: procset Puzzles 0 0"
373 print "%%EndComments"
374 print "%%BeginProlog"
375 print "%%BeginResource: procset Puzzles 0 0"
378 print " newpath 0 0 moveto (X) true charpath flattenpath pathbbox"
379 print " 3 -1 roll add 2 div 3 1 roll pop pop sub moveto"
380 print " dup stringwidth pop 0.5 mul neg 0 rmoveto show"
382 print "%%EndResource"
385 print "%%IncludeResource: font Helvetica"
391 for i
in xrange(1, pages
+1):
392 print "%%Page:", i
, i
395 # Do the drawing for each puzzle, giving a set of PS fragments
396 # and bounding boxes.
397 fragments
= [['' for i
in xrange(xx
)] for i
in xrange(yy
)]
398 lrbound
= [(0,0) for i
in xrange(xx
)]
399 tbbound
= [(0,0) for i
in xrange(yy
)]
403 if puzzle_index
>= len(ids
):
405 coords
, frag
= formatter(ids
[puzzle_index
])
406 fragments
[y
][x
] = frag
408 lrbound
[x
] = (max(lb
, coords
[0]), max(rb
, coords
[1]))
410 tbbound
[y
] = (max(tb
, coords
[2]), max(bb
, coords
[3]))
411 puzzle_index
= puzzle_index
+ 1
413 # Now we know the sizes of everything, do the drawing in such a
414 # way that we provide equal gutter space at the page edges and
415 # between puzzle rows/columns.
418 if len(fragments
[y
][x
]) > 0:
420 print "clippath flattenpath pathbbox pop pop translate"
421 print "clippath flattenpath pathbbox 4 2 roll pop pop"
422 # Compute the total height of all puzzles, which
423 # we'll use it to work out the amount of gutter
424 # space below this puzzle.
425 htotal
= reduce(lambda a
,b
:a
+b
, map(lambda (a
,b
):a
+b
, tbbound
), 0)
426 # Now compute the total height of all puzzles
427 # _below_ this one, plus the height-below-origin of
429 hbelow
= reduce(lambda a
,b
:a
+b
, map(lambda (a
,b
):a
+b
, tbbound
[y
+1:]), 0)
430 hbelow
= hbelow
+ tbbound
[y
][1]
431 print "%g sub %d mul %d div %g add exch" %
(htotal
, yy
-y
, yy
+1, hbelow
)
432 # Now do all the same computations for width,
433 # except we need the total width of everything
434 # _before_ this one since the coordinates work the
436 wtotal
= reduce(lambda a
,b
:a
+b
, map(lambda (a
,b
):a
+b
, lrbound
), 0)
437 # Now compute the total height of all puzzles
438 # _below_ this one, plus the height-below-origin of
440 wleft
= reduce(lambda a
,b
:a
+b
, map(lambda (a
,b
):a
+b
, lrbound
[:x
]), 0)
441 wleft
= wleft
+ lrbound
[x
][0]
442 print "%g sub %d mul %d div %g add exch" %
(wtotal
, x
+1, xx
+1, wleft
)
444 sys
.stdout
.write(fragments
[y
][x
])
447 print "restore showpage"