Document the mouse control method for Cube.
[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
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 210def 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
273def 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
336formatters = {
c3ea5dc7 337"net": net_format,
d91e1fc9 338"rect": rect_format,
339"rectangles": rect_format,
340"pattern": pattern_format,
341"solo": solo_format
342}
343
344if len(sys.argv) < 3:
345 sys.stderr.write("print.py: expected two arguments (game and format)\n")
346 sys.exit(1)
347
348formatter = formatters.get(sys.argv[1], None)
349if formatter == None:
350 sys.stderr.write("print.py: unrecognised game name `%s'\n" % sys.argv[1])
351 sys.exit(1)
352
353try:
354 format = map(string.atoi, string.split(sys.argv[2], "x"))
355except ValueError, e:
356 format = []
357if len(format) != 2:
358 sys.stderr.write("print.py: expected format such as `2x3' as second" \
359 + " argument\n")
360 sys.exit(1)
361
362xx, yy = format
363ppp = xx * yy # puzzles per page
364
365ids = []
366while 1:
367 s = sys.stdin.readline()
368 if s == "": break
369 if s[-1:] == "\n": s = s[:-1]
370 ids.append(s)
371
372pages = int((len(ids) + ppp - 1) / ppp)
373
374# Output initial DSC stuff.
375print "%!PS-Adobe-3.0"
376print "%%Creator: print.py from Simon Tatham's Puzzle Collection"
377print "%%DocumentData: Clean7Bit"
378print "%%LanguageLevel: 1"
379print "%%Pages:", pages
380print "%%DocumentNeededResources:"
381print "%%+ font Helvetica"
382print "%%DocumentSuppliedResources: procset Puzzles 0 0"
383print "%%EndComments"
384print "%%BeginProlog"
385print "%%BeginResource: procset Puzzles 0 0"
386print "/ctshow {"
387print " 3 1 roll"
388print " newpath 0 0 moveto (X) true charpath flattenpath pathbbox"
389print " 3 -1 roll add 2 div 3 1 roll pop pop sub moveto"
390print " dup stringwidth pop 0.5 mul neg 0 rmoveto show"
391print "} bind def"
392print "%%EndResource"
393print "%%EndProlog"
394print "%%BeginSetup"
395print "%%IncludeResource: font Helvetica"
396print "%%EndSetup"
397
398# Now do each page.
399puzzle_index = 0;
400
401for 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
459print "%%EOF"