Null-terminate generated Net/Netslide descriptive game IDs.
[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
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
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
95def 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
158def 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
221formatters = {
222"rect": rect_format,
223"rectangles": rect_format,
224"pattern": pattern_format,
225"solo": solo_format
226}
227
228if len(sys.argv) < 3:
229 sys.stderr.write("print.py: expected two arguments (game and format)\n")
230 sys.exit(1)
231
232formatter = formatters.get(sys.argv[1], None)
233if formatter == None:
234 sys.stderr.write("print.py: unrecognised game name `%s'\n" % sys.argv[1])
235 sys.exit(1)
236
237try:
238 format = map(string.atoi, string.split(sys.argv[2], "x"))
239except ValueError, e:
240 format = []
241if len(format) != 2:
242 sys.stderr.write("print.py: expected format such as `2x3' as second" \
243 + " argument\n")
244 sys.exit(1)
245
246xx, yy = format
247ppp = xx * yy # puzzles per page
248
249ids = []
250while 1:
251 s = sys.stdin.readline()
252 if s == "": break
253 if s[-1:] == "\n": s = s[:-1]
254 ids.append(s)
255
256pages = int((len(ids) + ppp - 1) / ppp)
257
258# Output initial DSC stuff.
259print "%!PS-Adobe-3.0"
260print "%%Creator: print.py from Simon Tatham's Puzzle Collection"
261print "%%DocumentData: Clean7Bit"
262print "%%LanguageLevel: 1"
263print "%%Pages:", pages
264print "%%DocumentNeededResources:"
265print "%%+ font Helvetica"
266print "%%DocumentSuppliedResources: procset Puzzles 0 0"
267print "%%EndComments"
268print "%%BeginProlog"
269print "%%BeginResource: procset Puzzles 0 0"
270print "/ctshow {"
271print " 3 1 roll"
272print " newpath 0 0 moveto (X) true charpath flattenpath pathbbox"
273print " 3 -1 roll add 2 div 3 1 roll pop pop sub moveto"
274print " dup stringwidth pop 0.5 mul neg 0 rmoveto show"
275print "} bind def"
276print "%%EndResource"
277print "%%EndProlog"
278print "%%BeginSetup"
279print "%%IncludeResource: font Helvetica"
280print "%%EndSetup"
281
282# Now do each page.
283puzzle_index = 0;
284
285for 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
343print "%%EOF"