chiark / gitweb /
solve_game() is passed the _initial_ game state, not the most recent
[sgt-puzzles.git] / print.py
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"