| 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" |