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 | |
26 | import sys |
27 | import string |
28 | import re |
29 | |
30 | class Holder: |
31 | pass |
32 | |
33 | def 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 | |
41 | def psprint(h, *a): |
42 | psvprint(h, a) |
43 | |
44 | def 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 |
95 | def 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 |
200 | def 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 | |
263 | def 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 | |
326 | formatters = { |
c3ea5dc7 |
327 | "net": net_format, |
d91e1fc9 |
328 | "rect": rect_format, |
329 | "rectangles": rect_format, |
330 | "pattern": pattern_format, |
331 | "solo": solo_format |
332 | } |
333 | |
334 | if len(sys.argv) < 3: |
335 | sys.stderr.write("print.py: expected two arguments (game and format)\n") |
336 | sys.exit(1) |
337 | |
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]) |
341 | sys.exit(1) |
342 | |
343 | try: |
344 | format = map(string.atoi, string.split(sys.argv[2], "x")) |
345 | except ValueError, e: |
346 | format = [] |
347 | if len(format) != 2: |
348 | sys.stderr.write("print.py: expected format such as `2x3' as second" \ |
349 | + " argument\n") |
350 | sys.exit(1) |
351 | |
352 | xx, yy = format |
353 | ppp = xx * yy # puzzles per page |
354 | |
355 | ids = [] |
356 | while 1: |
357 | s = sys.stdin.readline() |
358 | if s == "": break |
359 | if s[-1:] == "\n": s = s[:-1] |
360 | ids.append(s) |
361 | |
362 | pages = int((len(ids) + ppp - 1) / ppp) |
363 | |
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" |
376 | print "/ctshow {" |
377 | print " 3 1 roll" |
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" |
381 | print "} bind def" |
382 | print "%%EndResource" |
383 | print "%%EndProlog" |
384 | print "%%BeginSetup" |
385 | print "%%IncludeResource: font Helvetica" |
386 | print "%%EndSetup" |
387 | |
388 | # Now do each page. |
389 | puzzle_index = 0; |
390 | |
391 | for 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 | |
449 | print "%%EOF" |