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 |
5ad01ca4 |
126 | squaresize = 0.25 |
c3ea5dc7 |
127 | # Set up coordinate system. |
128 | pw = gridpitch * w |
129 | ph = gridpitch * h |
130 | ret.coords = (pw/2, pw/2, ph/2, ph/2) |
131 | psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2])) |
132 | # Draw the base grid lines. |
133 | psprint(ret, "newpath 0.02 setlinewidth") |
134 | for x in xrange(1,w): |
135 | psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, h * gridpitch)) |
136 | for y in xrange(1,h): |
137 | psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, w * gridpitch)) |
138 | psprint(ret, "stroke") |
139 | # Draw round the grid exterior. |
140 | psprint(ret, "newpath") |
141 | if not wrapping: |
142 | psprint(ret, "2 setlinewidth") |
143 | psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \ |
144 | (h * gridpitch, w * gridpitch, -h * gridpitch)) |
145 | psprint(ret, "closepath stroke") |
146 | # Draw any barriers. |
147 | psprint(ret, "newpath 2 setlinewidth 1 setlinecap") |
148 | for x, y in hbarriers: |
149 | psprint(ret, "%g %g moveto %g 0 rlineto" % \ |
150 | (x * gridpitch, (h - y) * gridpitch, gridpitch)) |
151 | for x, y in vbarriers: |
152 | psprint(ret, "%g %g moveto 0 -%g rlineto" % \ |
153 | (x * gridpitch, (h - y) * gridpitch, gridpitch)) |
154 | psprint(ret, "stroke") |
155 | # And draw the symbol in each box. |
156 | for i in xrange(len(grid)): |
157 | x = i % w |
158 | y = i / w |
159 | v = grid[i] |
160 | # Rotate to canonical form. |
161 | if v in (1,2,4,8): |
162 | v = 1 |
163 | elif v in (5,10): |
164 | v = 5 |
165 | elif v in (3,6,9,12): |
166 | v = 9 |
167 | elif v in (7,11,13,14): |
168 | v = 13 |
169 | # Centre on an area in the corner of the tile. |
170 | psprint(ret, "gsave") |
171 | if v & 4: |
172 | hoffset = bigoffset |
173 | else: |
174 | hoffset = smalloffset |
175 | if v & 2: |
176 | voffset = bigoffset |
177 | else: |
178 | voffset = smalloffset |
179 | psprint(ret, "%g %g translate" % \ |
180 | ((x + hoffset) * gridpitch, (h - y - voffset) * gridpitch)) |
181 | psprint(ret, "%g dup scale" % (float(gridpitch) * scale / 2)) |
182 | psprint(ret, "newpath 0.07 setlinewidth") |
183 | # Draw the radial lines. |
184 | for dx, dy, z in ((1,0,1), (0,1,2), (-1,0,4), (0,-1,8)): |
185 | if v & z: |
186 | psprint(ret, "0 0 moveto %d %d lineto" % (dx, dy)) |
187 | psprint(ret, "stroke") |
188 | # Draw additional figures if desired. |
5ad01ca4 |
189 | if v == 1: |
c3ea5dc7 |
190 | # Endpoints have a little empty square at the centre. |
5ad01ca4 |
191 | psprint(ret, "newpath %g %g moveto 0 -%g rlineto" % \ |
192 | (squaresize, squaresize, squaresize * 2)) |
193 | psprint(ret, "-%g 0 rlineto 0 %g rlineto closepath fill" % \ |
194 | (squaresize * 2, squaresize * 2)) |
195 | # Get back out of the centre section. |
c3ea5dc7 |
196 | psprint(ret, "grestore") |
5ad01ca4 |
197 | # Draw the endpoint square in large in the middle. |
198 | if v == 1: |
199 | psprint(ret, "gsave") |
200 | psprint(ret, "%g %g translate" % \ |
201 | ((x + 0.5) * gridpitch, (h - y - 0.5) * gridpitch)) |
202 | psprint(ret, "%g dup scale" % (float(gridpitch) / 2)) |
203 | psprint(ret, "newpath %g %g moveto 0 -%g rlineto" % \ |
204 | (squaresize, squaresize, squaresize * 2)) |
205 | psprint(ret, "-%g 0 rlineto 0 %g rlineto closepath fill" % \ |
206 | (squaresize * 2, squaresize * 2)) |
207 | psprint(ret, "grestore") |
c3ea5dc7 |
208 | return ret.coords, ret.s |
209 | |
d91e1fc9 |
210 | def pattern_format(s): |
211 | ret = Holder() |
212 | ret.s = "" |
213 | # Parse the game ID. |
214 | params, seed = string.split(s, ":") |
215 | w, h = map(string.atoi, string.split(params, "x")) |
216 | rowdata = map(lambda s: string.split(s, "."), string.split(seed, "/")) |
217 | assert len(rowdata) == w+h |
218 | # I'm going to arbitrarily choose to use 7pt text for the |
219 | # numbers, and a 14pt grid pitch. |
220 | textht = 7 |
221 | gridpitch = 14 |
222 | gutter = 8 # between the numbers and the grid |
223 | # Find the maximum number of numbers in each dimension, to |
224 | # determine the border size required. |
225 | xborder = reduce(max, map(len, rowdata[w:])) |
226 | yborder = reduce(max, map(len, rowdata[:w])) |
227 | # Set up coordinate system. I'm going to put the origin at the |
228 | # _top left_ of the grid, so that both sets of numbers get |
229 | # drawn the same way. |
230 | pw = (w + xborder) * gridpitch + gutter |
231 | ph = (h + yborder) * gridpitch + gutter |
232 | ret.coords = (xborder * gridpitch + gutter, w * gridpitch, \ |
233 | yborder * gridpitch + gutter, h * gridpitch) |
234 | # Draw the internal grid lines. Every fifth one is thicker, as |
235 | # a visual aid. |
236 | psprint(ret, "newpath 0.1 setlinewidth") |
237 | for x in xrange(1,w): |
238 | if x % 5 != 0: |
239 | psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, -h * gridpitch)) |
240 | for y in xrange(1,h): |
241 | if y % 5 != 0: |
242 | psprint(ret, "0 %g moveto %g 0 rlineto" % (-y * gridpitch, w * gridpitch)) |
243 | psprint(ret, "stroke") |
244 | psprint(ret, "newpath 0.75 setlinewidth") |
245 | for x in xrange(5,w,5): |
246 | psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, -h * gridpitch)) |
247 | for y in xrange(5,h,5): |
248 | psprint(ret, "0 %g moveto %g 0 rlineto" % (-y * gridpitch, w * gridpitch)) |
249 | psprint(ret, "stroke") |
250 | # Draw round the grid exterior. |
251 | psprint(ret, "newpath 1.5 setlinewidth") |
252 | psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \ |
253 | (-h * gridpitch, w * gridpitch, h * gridpitch)) |
254 | psprint(ret, "closepath stroke") |
255 | # And draw the numbers. |
256 | psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht) |
257 | for i in range(w+h): |
258 | ns = rowdata[i] |
259 | if i < w: |
260 | xo = (i + 0.5) * gridpitch |
261 | yo = (gutter + 0.5 * gridpitch) |
262 | else: |
263 | xo = -(gutter + 0.5 * gridpitch) |
264 | yo = ((i-w) + 0.5) * -gridpitch |
265 | for j in range(len(ns)-1, -1, -1): |
266 | psprint(ret, "%g %g (%s) ctshow" % (xo, yo, ns[j])) |
267 | if i < w: |
268 | yo = yo + gridpitch |
269 | else: |
270 | xo = xo - gridpitch |
271 | return ret.coords, ret.s |
272 | |
273 | def solo_format(s): |
274 | ret = Holder() |
275 | ret.s = "" |
276 | # Parse the game ID. |
277 | params, seed = string.split(s, ":") |
278 | c, r = map(string.atoi, string.split(params, "x")) |
279 | cr = c*r |
280 | grid = [] |
281 | while len(seed) > 0: |
282 | if seed[0] in '_'+string.lowercase: |
283 | if seed[0] in string.lowercase: |
284 | grid.extend([-1] * (ord(seed[0]) - ord('a') + 1)) |
285 | seed = seed[1:] |
286 | elif seed[0] in string.digits: |
287 | ns = "" |
288 | while len(seed) > 0 and seed[0] in string.digits: |
289 | ns = ns + seed[0] |
290 | seed = seed[1:] |
291 | grid.append(string.atoi(ns)) |
292 | assert cr * cr == len(grid) |
293 | # I'm going to arbitrarily choose to use 9pt text for the |
294 | # numbers, and a 16pt grid pitch. |
295 | textht = 9 |
296 | gridpitch = 16 |
297 | # Set up coordinate system. |
298 | pw = ph = gridpitch * cr |
299 | ret.coords = (pw/2, pw/2, ph/2, ph/2) |
300 | psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2])) |
301 | # Draw the thin internal grid lines. |
302 | psprint(ret, "newpath 0.1 setlinewidth") |
303 | for x in xrange(1,cr): |
304 | if x % r != 0: |
305 | psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, cr * gridpitch)) |
306 | for y in xrange(1,cr): |
307 | if y % c != 0: |
308 | psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, cr * gridpitch)) |
309 | psprint(ret, "stroke") |
310 | # Draw the thicker internal grid lines. |
311 | psprint(ret, "newpath 1 setlinewidth") |
312 | for x in xrange(r,cr,r): |
313 | psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, cr * gridpitch)) |
314 | for y in xrange(c,cr,c): |
315 | psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, cr * gridpitch)) |
316 | psprint(ret, "stroke") |
317 | # Draw round the grid exterior, thicker still. |
318 | psprint(ret, "newpath 1.5 setlinewidth") |
319 | psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \ |
320 | (cr * gridpitch, cr * gridpitch, -cr * gridpitch)) |
321 | psprint(ret, "closepath stroke") |
322 | # And draw the numbers. |
323 | psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht) |
324 | for y in xrange(cr): |
325 | for x in xrange(cr): |
326 | n = grid[y*cr+x] |
327 | if n > 0: |
328 | if n > 9: |
329 | s = chr(ord('a') + n - 10) |
330 | else: |
331 | s = chr(ord('0') + n) |
332 | psprint(ret, "%g %g (%s) ctshow" % \ |
333 | ((x+0.5)*gridpitch, (cr-y-0.5)*gridpitch, s)) |
334 | return ret.coords, ret.s |
335 | |
336 | formatters = { |
c3ea5dc7 |
337 | "net": net_format, |
d91e1fc9 |
338 | "rect": rect_format, |
339 | "rectangles": rect_format, |
340 | "pattern": pattern_format, |
341 | "solo": solo_format |
342 | } |
343 | |
344 | if len(sys.argv) < 3: |
345 | sys.stderr.write("print.py: expected two arguments (game and format)\n") |
346 | sys.exit(1) |
347 | |
348 | formatter = formatters.get(sys.argv[1], None) |
349 | if formatter == None: |
350 | sys.stderr.write("print.py: unrecognised game name `%s'\n" % sys.argv[1]) |
351 | sys.exit(1) |
352 | |
353 | try: |
354 | format = map(string.atoi, string.split(sys.argv[2], "x")) |
355 | except ValueError, e: |
356 | format = [] |
357 | if len(format) != 2: |
358 | sys.stderr.write("print.py: expected format such as `2x3' as second" \ |
359 | + " argument\n") |
360 | sys.exit(1) |
361 | |
362 | xx, yy = format |
363 | ppp = xx * yy # puzzles per page |
364 | |
365 | ids = [] |
366 | while 1: |
367 | s = sys.stdin.readline() |
368 | if s == "": break |
369 | if s[-1:] == "\n": s = s[:-1] |
370 | ids.append(s) |
371 | |
372 | pages = int((len(ids) + ppp - 1) / ppp) |
373 | |
374 | # Output initial DSC stuff. |
375 | print "%!PS-Adobe-3.0" |
376 | print "%%Creator: print.py from Simon Tatham's Puzzle Collection" |
377 | print "%%DocumentData: Clean7Bit" |
378 | print "%%LanguageLevel: 1" |
379 | print "%%Pages:", pages |
380 | print "%%DocumentNeededResources:" |
381 | print "%%+ font Helvetica" |
382 | print "%%DocumentSuppliedResources: procset Puzzles 0 0" |
383 | print "%%EndComments" |
384 | print "%%BeginProlog" |
385 | print "%%BeginResource: procset Puzzles 0 0" |
386 | print "/ctshow {" |
387 | print " 3 1 roll" |
388 | print " newpath 0 0 moveto (X) true charpath flattenpath pathbbox" |
389 | print " 3 -1 roll add 2 div 3 1 roll pop pop sub moveto" |
390 | print " dup stringwidth pop 0.5 mul neg 0 rmoveto show" |
391 | print "} bind def" |
392 | print "%%EndResource" |
393 | print "%%EndProlog" |
394 | print "%%BeginSetup" |
395 | print "%%IncludeResource: font Helvetica" |
396 | print "%%EndSetup" |
397 | |
398 | # Now do each page. |
399 | puzzle_index = 0; |
400 | |
401 | for i in xrange(1, pages+1): |
402 | print "%%Page:", i, i |
403 | print "save" |
404 | |
405 | # Do the drawing for each puzzle, giving a set of PS fragments |
406 | # and bounding boxes. |
407 | fragments = [['' for i in xrange(xx)] for i in xrange(yy)] |
408 | lrbound = [(0,0) for i in xrange(xx)] |
409 | tbbound = [(0,0) for i in xrange(yy)] |
410 | |
411 | for y in xrange(yy): |
412 | for x in xrange(xx): |
413 | if puzzle_index >= len(ids): |
414 | break |
415 | coords, frag = formatter(ids[puzzle_index]) |
416 | fragments[y][x] = frag |
417 | lb, rb = lrbound[x] |
418 | lrbound[x] = (max(lb, coords[0]), max(rb, coords[1])) |
419 | tb, bb = tbbound[y] |
420 | tbbound[y] = (max(tb, coords[2]), max(bb, coords[3])) |
421 | puzzle_index = puzzle_index + 1 |
422 | |
423 | # Now we know the sizes of everything, do the drawing in such a |
424 | # way that we provide equal gutter space at the page edges and |
425 | # between puzzle rows/columns. |
426 | for y in xrange(yy): |
427 | for x in xrange(xx): |
428 | if len(fragments[y][x]) > 0: |
429 | print "gsave" |
430 | print "clippath flattenpath pathbbox pop pop translate" |
431 | print "clippath flattenpath pathbbox 4 2 roll pop pop" |
432 | # Compute the total height of all puzzles, which |
433 | # we'll use it to work out the amount of gutter |
434 | # space below this puzzle. |
435 | htotal = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, tbbound), 0) |
436 | # Now compute the total height of all puzzles |
437 | # _below_ this one, plus the height-below-origin of |
438 | # this one. |
439 | hbelow = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, tbbound[y+1:]), 0) |
440 | hbelow = hbelow + tbbound[y][1] |
441 | print "%g sub %d mul %d div %g add exch" % (htotal, yy-y, yy+1, hbelow) |
442 | # Now do all the same computations for width, |
443 | # except we need the total width of everything |
444 | # _before_ this one since the coordinates work the |
445 | # other way round. |
446 | wtotal = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, lrbound), 0) |
447 | # Now compute the total height of all puzzles |
448 | # _below_ this one, plus the height-below-origin of |
449 | # this one. |
450 | wleft = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, lrbound[:x]), 0) |
451 | wleft = wleft + lrbound[x][0] |
452 | print "%g sub %d mul %d div %g add exch" % (wtotal, x+1, xx+1, wleft) |
453 | print "translate" |
454 | sys.stdout.write(fragments[y][x]) |
455 | print "grestore" |
456 | |
457 | print "restore showpage" |
458 | |
459 | print "%%EOF" |