Forgot to add print.py to the source archive. Ahem.
[sgt/puzzles] / print.py
CommitLineData
d91e1fc9 1#!/usr/bin/env python
2
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.
7
8# Supported games are those which are sensibly solvable using
c3ea5dc7 9# pencil and paper: Rectangles, Pattern, Solo, Net.
d91e1fc9 10
11# Command-line syntax is
12#
13# print.py <game-name> <format>
14#
c3ea5dc7 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.
d91e1fc9 18#
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.
21#
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.
25
26import sys
27import string
28import re
29
30class Holder:
31 pass
32
33def psvprint(h, a):
34 for i in xrange(len(a)):
35 h.s = h.s + str(a[i])
36 if i < len(a)-1:
37 h.s = h.s + " "
38 else:
39 h.s = h.s + "\n"
40
41def psprint(h, *a):
42 psvprint(h, a)
43
44def rect_format(s):
45 # Parse the game ID.
46 ret = Holder()
47 ret.s = ""
48 params, seed = string.split(s, ":")
49 w, h = map(string.atoi, string.split(params, "x"))
50 grid = []
51 while len(seed) > 0:
52 if seed[0] in '_'+string.lowercase:
53 if seed[0] in string.lowercase:
54 grid.extend([-1] * (ord(seed[0]) - ord('a') + 1))
55 seed = seed[1:]
56 elif seed[0] in string.digits:
57 ns = ""
58 while len(seed) > 0 and seed[0] in string.digits:
59 ns = ns + seed[0]
60 seed = seed[1:]
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.
65 textht = 7
66 gridpitch = 14
67 # Set up coordinate system.
68 pw = gridpitch * w
69 ph = gridpitch * h
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")
75 for x in xrange(1,w):
76 psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, h * gridpitch))
77 for y in xrange(1,h):
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)
87 for y in xrange(h):
88 for x in xrange(w):
89 n = grid[y*w+x]
90 if n > 0:
91 psprint(ret, "%g %g (%d) ctshow" % \
92 ((x+0.5)*gridpitch, (h-y-0.5)*gridpitch, n))
93 return ret.coords, ret.s
94
c3ea5dc7 95def net_format(s):
96 # Parse the game ID.
97 ret = Holder()
98 ret.s = ""
99 params, seed = string.split(s, ":")
100 wrapping = 0
101 if params[-1:] == "w":
102 wrapping = 1
103 params = params[:-1]
104 w, h = map(string.atoi, string.split(params, "x"))
105 grid = []
106 hbarriers = []
107 vbarriers = []
108 while len(seed) > 0:
109 n = string.atoi(seed[0], 16)
110 seed = seed[1:]
111 while len(seed) > 0 and seed[0] in 'hv':
112 x = len(grid) % w
113 y = len(grid) / w
114 if seed[0] == 'h':
115 hbarriers.append((x, y+1))
116 else:
117 vbarriers.append((x+1, y))
118 seed = seed[1:]
119 grid.append(n)
120 assert w * h == len(grid)
121 # I'm going to arbitrarily choose a 24pt grid pitch.
122 gridpitch = 24
123 scale = 0.25
124 bigoffset = 0.25
125 smalloffset = 0.17
126 # Set up coordinate system.
127 pw = gridpitch * w
128 ph = gridpitch * h
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")
140 if not wrapping:
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")
145 # Draw any barriers.
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)):
156 x = i % w
157 y = i / w
158 v = grid[i]
159 # Rotate to canonical form.
160 if v in (1,2,4,8):
161 v = 1
162 elif v in (5,10):
163 v = 5
164 elif v in (3,6,9,12):
165 v = 9
166 elif v in (7,11,13,14):
167 v = 13
168 # Centre on an area in the corner of the tile.
169 psprint(ret, "gsave")
170 if v & 4:
171 hoffset = bigoffset
172 else:
173 hoffset = smalloffset
174 if v & 2:
175 voffset = bigoffset
176 else:
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)):
184 if v & z:
185 psprint(ret, "0 0 moveto %d %d lineto" % (dx, dy))
186 psprint(ret, "stroke")
187 # Draw additional figures if desired.
188 if v == 13:
189 # T-pieces have a little circular blob where the lines join.
190 psprint(ret, "newpath 0 0 0.15 0 360 arc fill")
191 elif v == 1:
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")
196 # Clean up.
197 psprint(ret, "grestore")
198 return ret.coords, ret.s
199
d91e1fc9 200def pattern_format(s):
201 ret = Holder()
202 ret.s = ""
203 # Parse the game ID.
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.
210 textht = 7
211 gridpitch = 14
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
225 # a visual aid.
226 psprint(ret, "newpath 0.1 setlinewidth")
227 for x in xrange(1,w):
228 if x % 5 != 0:
229 psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, -h * gridpitch))
230 for y in xrange(1,h):
231 if y % 5 != 0:
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)
247 for i in range(w+h):
248 ns = rowdata[i]
249 if i < w:
250 xo = (i + 0.5) * gridpitch
251 yo = (gutter + 0.5 * gridpitch)
252 else:
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]))
257 if i < w:
258 yo = yo + gridpitch
259 else:
260 xo = xo - gridpitch
261 return ret.coords, ret.s
262
263def solo_format(s):
264 ret = Holder()
265 ret.s = ""
266 # Parse the game ID.
267 params, seed = string.split(s, ":")
268 c, r = map(string.atoi, string.split(params, "x"))
269 cr = c*r
270 grid = []
271 while len(seed) > 0:
272 if seed[0] in '_'+string.lowercase:
273 if seed[0] in string.lowercase:
274 grid.extend([-1] * (ord(seed[0]) - ord('a') + 1))
275 seed = seed[1:]
276 elif seed[0] in string.digits:
277 ns = ""
278 while len(seed) > 0 and seed[0] in string.digits:
279 ns = ns + seed[0]
280 seed = seed[1:]
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.
285 textht = 9
286 gridpitch = 16
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):
294 if x % r != 0:
295 psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, cr * gridpitch))
296 for y in xrange(1,cr):
297 if y % c != 0:
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)
314 for y in xrange(cr):
315 for x in xrange(cr):
316 n = grid[y*cr+x]
317 if n > 0:
318 if n > 9:
319 s = chr(ord('a') + n - 10)
320 else:
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
325
326formatters = {
c3ea5dc7 327"net": net_format,
d91e1fc9 328"rect": rect_format,
329"rectangles": rect_format,
330"pattern": pattern_format,
331"solo": solo_format
332}
333
334if len(sys.argv) < 3:
335 sys.stderr.write("print.py: expected two arguments (game and format)\n")
336 sys.exit(1)
337
338formatter = formatters.get(sys.argv[1], None)
339if formatter == None:
340 sys.stderr.write("print.py: unrecognised game name `%s'\n" % sys.argv[1])
341 sys.exit(1)
342
343try:
344 format = map(string.atoi, string.split(sys.argv[2], "x"))
345except ValueError, e:
346 format = []
347if len(format) != 2:
348 sys.stderr.write("print.py: expected format such as `2x3' as second" \
349 + " argument\n")
350 sys.exit(1)
351
352xx, yy = format
353ppp = xx * yy # puzzles per page
354
355ids = []
356while 1:
357 s = sys.stdin.readline()
358 if s == "": break
359 if s[-1:] == "\n": s = s[:-1]
360 ids.append(s)
361
362pages = int((len(ids) + ppp - 1) / ppp)
363
364# Output initial DSC stuff.
365print "%!PS-Adobe-3.0"
366print "%%Creator: print.py from Simon Tatham's Puzzle Collection"
367print "%%DocumentData: Clean7Bit"
368print "%%LanguageLevel: 1"
369print "%%Pages:", pages
370print "%%DocumentNeededResources:"
371print "%%+ font Helvetica"
372print "%%DocumentSuppliedResources: procset Puzzles 0 0"
373print "%%EndComments"
374print "%%BeginProlog"
375print "%%BeginResource: procset Puzzles 0 0"
376print "/ctshow {"
377print " 3 1 roll"
378print " newpath 0 0 moveto (X) true charpath flattenpath pathbbox"
379print " 3 -1 roll add 2 div 3 1 roll pop pop sub moveto"
380print " dup stringwidth pop 0.5 mul neg 0 rmoveto show"
381print "} bind def"
382print "%%EndResource"
383print "%%EndProlog"
384print "%%BeginSetup"
385print "%%IncludeResource: font Helvetica"
386print "%%EndSetup"
387
388# Now do each page.
389puzzle_index = 0;
390
391for i in xrange(1, pages+1):
392 print "%%Page:", i, i
393 print "save"
394
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)]
400
401 for y in xrange(yy):
402 for x in xrange(xx):
403 if puzzle_index >= len(ids):
404 break
405 coords, frag = formatter(ids[puzzle_index])
406 fragments[y][x] = frag
407 lb, rb = lrbound[x]
408 lrbound[x] = (max(lb, coords[0]), max(rb, coords[1]))
409 tb, bb = tbbound[y]
410 tbbound[y] = (max(tb, coords[2]), max(bb, coords[3]))
411 puzzle_index = puzzle_index + 1
412
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.
416 for y in xrange(yy):
417 for x in xrange(xx):
418 if len(fragments[y][x]) > 0:
419 print "gsave"
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
428 # this one.
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
435 # other way round.
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
439 # this one.
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)
443 print "translate"
444 sys.stdout.write(fragments[y][x])
445 print "grestore"
446
447 print "restore showpage"
448
449print "%%EOF"