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 |
9 | # pencil and paper: Rectangles, Pattern and Solo. |
10 | |
11 | # Command-line syntax is |
12 | # |
13 | # print.py <game-name> <format> |
14 | # |
15 | # <game-name> is one of `rect', `rectangles', `pattern', `solo'. |
16 | # <format> is two numbers separated by an x: `2x3', for example, |
17 | # means two columns by three rows. |
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 | |
95 | def pattern_format(s): |
96 | ret = Holder() |
97 | ret.s = "" |
98 | # Parse the game ID. |
99 | params, seed = string.split(s, ":") |
100 | w, h = map(string.atoi, string.split(params, "x")) |
101 | rowdata = map(lambda s: string.split(s, "."), string.split(seed, "/")) |
102 | assert len(rowdata) == w+h |
103 | # I'm going to arbitrarily choose to use 7pt text for the |
104 | # numbers, and a 14pt grid pitch. |
105 | textht = 7 |
106 | gridpitch = 14 |
107 | gutter = 8 # between the numbers and the grid |
108 | # Find the maximum number of numbers in each dimension, to |
109 | # determine the border size required. |
110 | xborder = reduce(max, map(len, rowdata[w:])) |
111 | yborder = reduce(max, map(len, rowdata[:w])) |
112 | # Set up coordinate system. I'm going to put the origin at the |
113 | # _top left_ of the grid, so that both sets of numbers get |
114 | # drawn the same way. |
115 | pw = (w + xborder) * gridpitch + gutter |
116 | ph = (h + yborder) * gridpitch + gutter |
117 | ret.coords = (xborder * gridpitch + gutter, w * gridpitch, \ |
118 | yborder * gridpitch + gutter, h * gridpitch) |
119 | # Draw the internal grid lines. Every fifth one is thicker, as |
120 | # a visual aid. |
121 | psprint(ret, "newpath 0.1 setlinewidth") |
122 | for x in xrange(1,w): |
123 | if x % 5 != 0: |
124 | psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, -h * gridpitch)) |
125 | for y in xrange(1,h): |
126 | if y % 5 != 0: |
127 | psprint(ret, "0 %g moveto %g 0 rlineto" % (-y * gridpitch, w * gridpitch)) |
128 | psprint(ret, "stroke") |
129 | psprint(ret, "newpath 0.75 setlinewidth") |
130 | for x in xrange(5,w,5): |
131 | psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, -h * gridpitch)) |
132 | for y in xrange(5,h,5): |
133 | psprint(ret, "0 %g moveto %g 0 rlineto" % (-y * gridpitch, w * gridpitch)) |
134 | psprint(ret, "stroke") |
135 | # Draw round the grid exterior. |
136 | psprint(ret, "newpath 1.5 setlinewidth") |
137 | psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \ |
138 | (-h * gridpitch, w * gridpitch, h * gridpitch)) |
139 | psprint(ret, "closepath stroke") |
140 | # And draw the numbers. |
141 | psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht) |
142 | for i in range(w+h): |
143 | ns = rowdata[i] |
144 | if i < w: |
145 | xo = (i + 0.5) * gridpitch |
146 | yo = (gutter + 0.5 * gridpitch) |
147 | else: |
148 | xo = -(gutter + 0.5 * gridpitch) |
149 | yo = ((i-w) + 0.5) * -gridpitch |
150 | for j in range(len(ns)-1, -1, -1): |
151 | psprint(ret, "%g %g (%s) ctshow" % (xo, yo, ns[j])) |
152 | if i < w: |
153 | yo = yo + gridpitch |
154 | else: |
155 | xo = xo - gridpitch |
156 | return ret.coords, ret.s |
157 | |
158 | def solo_format(s): |
159 | ret = Holder() |
160 | ret.s = "" |
161 | # Parse the game ID. |
162 | params, seed = string.split(s, ":") |
163 | c, r = map(string.atoi, string.split(params, "x")) |
164 | cr = c*r |
165 | grid = [] |
166 | while len(seed) > 0: |
167 | if seed[0] in '_'+string.lowercase: |
168 | if seed[0] in string.lowercase: |
169 | grid.extend([-1] * (ord(seed[0]) - ord('a') + 1)) |
170 | seed = seed[1:] |
171 | elif seed[0] in string.digits: |
172 | ns = "" |
173 | while len(seed) > 0 and seed[0] in string.digits: |
174 | ns = ns + seed[0] |
175 | seed = seed[1:] |
176 | grid.append(string.atoi(ns)) |
177 | assert cr * cr == len(grid) |
178 | # I'm going to arbitrarily choose to use 9pt text for the |
179 | # numbers, and a 16pt grid pitch. |
180 | textht = 9 |
181 | gridpitch = 16 |
182 | # Set up coordinate system. |
183 | pw = ph = gridpitch * cr |
184 | ret.coords = (pw/2, pw/2, ph/2, ph/2) |
185 | psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2])) |
186 | # Draw the thin internal grid lines. |
187 | psprint(ret, "newpath 0.1 setlinewidth") |
188 | for x in xrange(1,cr): |
189 | if x % r != 0: |
190 | psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, cr * gridpitch)) |
191 | for y in xrange(1,cr): |
192 | if y % c != 0: |
193 | psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, cr * gridpitch)) |
194 | psprint(ret, "stroke") |
195 | # Draw the thicker internal grid lines. |
196 | psprint(ret, "newpath 1 setlinewidth") |
197 | for x in xrange(r,cr,r): |
198 | psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, cr * gridpitch)) |
199 | for y in xrange(c,cr,c): |
200 | psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, cr * gridpitch)) |
201 | psprint(ret, "stroke") |
202 | # Draw round the grid exterior, thicker still. |
203 | psprint(ret, "newpath 1.5 setlinewidth") |
204 | psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \ |
205 | (cr * gridpitch, cr * gridpitch, -cr * gridpitch)) |
206 | psprint(ret, "closepath stroke") |
207 | # And draw the numbers. |
208 | psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht) |
209 | for y in xrange(cr): |
210 | for x in xrange(cr): |
211 | n = grid[y*cr+x] |
212 | if n > 0: |
213 | if n > 9: |
214 | s = chr(ord('a') + n - 10) |
215 | else: |
216 | s = chr(ord('0') + n) |
217 | psprint(ret, "%g %g (%s) ctshow" % \ |
218 | ((x+0.5)*gridpitch, (cr-y-0.5)*gridpitch, s)) |
219 | return ret.coords, ret.s |
220 | |
221 | formatters = { |
222 | "rect": rect_format, |
223 | "rectangles": rect_format, |
224 | "pattern": pattern_format, |
225 | "solo": solo_format |
226 | } |
227 | |
228 | if len(sys.argv) < 3: |
229 | sys.stderr.write("print.py: expected two arguments (game and format)\n") |
230 | sys.exit(1) |
231 | |
232 | formatter = formatters.get(sys.argv[1], None) |
233 | if formatter == None: |
234 | sys.stderr.write("print.py: unrecognised game name `%s'\n" % sys.argv[1]) |
235 | sys.exit(1) |
236 | |
237 | try: |
238 | format = map(string.atoi, string.split(sys.argv[2], "x")) |
239 | except ValueError, e: |
240 | format = [] |
241 | if len(format) != 2: |
242 | sys.stderr.write("print.py: expected format such as `2x3' as second" \ |
243 | + " argument\n") |
244 | sys.exit(1) |
245 | |
246 | xx, yy = format |
247 | ppp = xx * yy # puzzles per page |
248 | |
249 | ids = [] |
250 | while 1: |
251 | s = sys.stdin.readline() |
252 | if s == "": break |
253 | if s[-1:] == "\n": s = s[:-1] |
254 | ids.append(s) |
255 | |
256 | pages = int((len(ids) + ppp - 1) / ppp) |
257 | |
258 | # Output initial DSC stuff. |
259 | print "%!PS-Adobe-3.0" |
260 | print "%%Creator: print.py from Simon Tatham's Puzzle Collection" |
261 | print "%%DocumentData: Clean7Bit" |
262 | print "%%LanguageLevel: 1" |
263 | print "%%Pages:", pages |
264 | print "%%DocumentNeededResources:" |
265 | print "%%+ font Helvetica" |
266 | print "%%DocumentSuppliedResources: procset Puzzles 0 0" |
267 | print "%%EndComments" |
268 | print "%%BeginProlog" |
269 | print "%%BeginResource: procset Puzzles 0 0" |
270 | print "/ctshow {" |
271 | print " 3 1 roll" |
272 | print " newpath 0 0 moveto (X) true charpath flattenpath pathbbox" |
273 | print " 3 -1 roll add 2 div 3 1 roll pop pop sub moveto" |
274 | print " dup stringwidth pop 0.5 mul neg 0 rmoveto show" |
275 | print "} bind def" |
276 | print "%%EndResource" |
277 | print "%%EndProlog" |
278 | print "%%BeginSetup" |
279 | print "%%IncludeResource: font Helvetica" |
280 | print "%%EndSetup" |
281 | |
282 | # Now do each page. |
283 | puzzle_index = 0; |
284 | |
285 | for i in xrange(1, pages+1): |
286 | print "%%Page:", i, i |
287 | print "save" |
288 | |
289 | # Do the drawing for each puzzle, giving a set of PS fragments |
290 | # and bounding boxes. |
291 | fragments = [['' for i in xrange(xx)] for i in xrange(yy)] |
292 | lrbound = [(0,0) for i in xrange(xx)] |
293 | tbbound = [(0,0) for i in xrange(yy)] |
294 | |
295 | for y in xrange(yy): |
296 | for x in xrange(xx): |
297 | if puzzle_index >= len(ids): |
298 | break |
299 | coords, frag = formatter(ids[puzzle_index]) |
300 | fragments[y][x] = frag |
301 | lb, rb = lrbound[x] |
302 | lrbound[x] = (max(lb, coords[0]), max(rb, coords[1])) |
303 | tb, bb = tbbound[y] |
304 | tbbound[y] = (max(tb, coords[2]), max(bb, coords[3])) |
305 | puzzle_index = puzzle_index + 1 |
306 | |
307 | # Now we know the sizes of everything, do the drawing in such a |
308 | # way that we provide equal gutter space at the page edges and |
309 | # between puzzle rows/columns. |
310 | for y in xrange(yy): |
311 | for x in xrange(xx): |
312 | if len(fragments[y][x]) > 0: |
313 | print "gsave" |
314 | print "clippath flattenpath pathbbox pop pop translate" |
315 | print "clippath flattenpath pathbbox 4 2 roll pop pop" |
316 | # Compute the total height of all puzzles, which |
317 | # we'll use it to work out the amount of gutter |
318 | # space below this puzzle. |
319 | htotal = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, tbbound), 0) |
320 | # Now compute the total height of all puzzles |
321 | # _below_ this one, plus the height-below-origin of |
322 | # this one. |
323 | hbelow = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, tbbound[y+1:]), 0) |
324 | hbelow = hbelow + tbbound[y][1] |
325 | print "%g sub %d mul %d div %g add exch" % (htotal, yy-y, yy+1, hbelow) |
326 | # Now do all the same computations for width, |
327 | # except we need the total width of everything |
328 | # _before_ this one since the coordinates work the |
329 | # other way round. |
330 | wtotal = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, lrbound), 0) |
331 | # Now compute the total height of all puzzles |
332 | # _below_ this one, plus the height-below-origin of |
333 | # this one. |
334 | wleft = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, lrbound[:x]), 0) |
335 | wleft = wleft + lrbound[x][0] |
336 | print "%g sub %d mul %d div %g add exch" % (wtotal, x+1, xx+1, wleft) |
337 | print "translate" |
338 | sys.stdout.write(fragments[y][x]) |
339 | print "grestore" |
340 | |
341 | print "restore showpage" |
342 | |
343 | print "%%EOF" |