ref: f44e4acd4a4216700ef8094cc4ae7251c6f4cdc6
dir: /print.py/
#!/usr/bin/env python # This program accepts a series of newline-separated game IDs on # stdin and formats them into PostScript to be printed out. You # specify using command-line options which game the IDs are for, # and how many you want per page. # Supported games are those which are sensibly solvable using # pencil and paper: Rectangles, Pattern, Solo, Net. # Command-line syntax is # # print.py <game-name> <format> # # <game-name> is one of `rect', `rectangles', `pattern', `solo', # `net', `dominosa'. <format> is two numbers separated by an x: # `2x3', for example, means two columns by three rows. # # The program will then read game IDs from stdin until it sees EOF, # and generate as many PostScript pages on stdout as it needs. # # The resulting PostScript will automatically adapt itself to the # size of the clip rectangle, so that the puzzles are sensibly # distributed across whatever paper size you decide to use. import sys import string import re class Holder: pass def psvprint(h, a): for i in xrange(len(a)): h.s = h.s + str(a[i]) if i < len(a)-1: h.s = h.s + " " else: h.s = h.s + "\n" def psprint(h, *a): psvprint(h, a) def rect_format(s): # Parse the game ID. ret = Holder() ret.s = "" params, seed = string.split(s, ":") w, h = map(string.atoi, string.split(params, "x")) grid = [] while len(seed) > 0: if seed[0] in '_'+string.lowercase: if seed[0] in string.lowercase: grid.extend([-1] * (ord(seed[0]) - ord('a') + 1)) seed = seed[1:] elif seed[0] in string.digits: ns = "" while len(seed) > 0 and seed[0] in string.digits: ns = ns + seed[0] seed = seed[1:] grid.append(string.atoi(ns)) assert w * h == len(grid) # I'm going to arbitrarily choose to use 7pt text for the # numbers, and a 14pt grid pitch. textht = 7 gridpitch = 14 # Set up coordinate system. pw = gridpitch * w ph = gridpitch * h ret.coords = (pw/2, pw/2, ph/2, ph/2) psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2])) # Draw the internal grid lines, _very_ thin (the player will # need to draw over them visibly). psprint(ret, "newpath 0.01 setlinewidth") for x in xrange(1,w): psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, h * gridpitch)) for y in xrange(1,h): psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, w * gridpitch)) psprint(ret, "stroke") # Draw round the grid exterior, much thicker. psprint(ret, "newpath 1.5 setlinewidth") psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \ (h * gridpitch, w * gridpitch, -h * gridpitch)) psprint(ret, "closepath stroke") # And draw the numbers. psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht) for y in xrange(h): for x in xrange(w): n = grid[y*w+x] if n > 0: psprint(ret, "%g %g (%d) ctshow" % \ ((x+0.5)*gridpitch, (h-y-0.5)*gridpitch, n)) return ret.coords, ret.s def net_format(s): # Parse the game ID. ret = Holder() ret.s = "" params, seed = string.split(s, ":") wrapping = 0 if params[-1:] == "w": wrapping = 1 params = params[:-1] w, h = map(string.atoi, string.split(params, "x")) grid = [] hbarriers = [] vbarriers = [] while len(seed) > 0: n = string.atoi(seed[0], 16) seed = seed[1:] while len(seed) > 0 and seed[0] in 'hv': x = len(grid) % w y = len(grid) / w if seed[0] == 'h': hbarriers.append((x, y+1)) else: vbarriers.append((x+1, y)) seed = seed[1:] grid.append(n) assert w * h == len(grid) # I'm going to arbitrarily choose a 24pt grid pitch. gridpitch = 24 scale = 0.25 bigoffset = 0.25 smalloffset = 0.17 squaresize = 0.25 # Set up coordinate system. pw = gridpitch * w ph = gridpitch * h ret.coords = (pw/2, pw/2, ph/2, ph/2) psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2])) # Draw the base grid lines. psprint(ret, "newpath 0.02 setlinewidth") for x in xrange(1,w): psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, h * gridpitch)) for y in xrange(1,h): psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, w * gridpitch)) psprint(ret, "stroke") # Draw round the grid exterior. psprint(ret, "newpath") if not wrapping: psprint(ret, "2 setlinewidth") psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \ (h * gridpitch, w * gridpitch, -h * gridpitch)) psprint(ret, "closepath stroke") # Draw any barriers. psprint(ret, "newpath 2 setlinewidth 1 setlinecap") for x, y in hbarriers: psprint(ret, "%g %g moveto %g 0 rlineto" % \ (x * gridpitch, (h - y) * gridpitch, gridpitch)) for x, y in vbarriers: psprint(ret, "%g %g moveto 0 -%g rlineto" % \ (x * gridpitch, (h - y) * gridpitch, gridpitch)) psprint(ret, "stroke") # And draw the symbol in each box. for i in xrange(len(grid)): x = i % w y = i / w v = grid[i] # Rotate to canonical form. if v in (1,2,4,8): v = 1 elif v in (5,10): v = 5 elif v in (3,6,9,12): v = 9 elif v in (7,11,13,14): v = 13 # Centre on an area in the corner of the tile. psprint(ret, "gsave") if v & 4: hoffset = bigoffset else: hoffset = smalloffset if v & 2: voffset = bigoffset else: voffset = smalloffset psprint(ret, "%g %g translate" % \ ((x + hoffset) * gridpitch, (h - y - voffset) * gridpitch)) psprint(ret, "%g dup scale" % (float(gridpitch) * scale / 2)) psprint(ret, "newpath 0.07 setlinewidth") # Draw the radial lines. for dx, dy, z in ((1,0,1), (0,1,2), (-1,0,4), (0,-1,8)): if v & z: psprint(ret, "0 0 moveto %d %d lineto" % (dx, dy)) psprint(ret, "stroke") # Draw additional figures if desired. if v == 1: # Endpoints have a little empty square at the centre. psprint(ret, "newpath %g %g moveto 0 -%g rlineto" % \ (squaresize, squaresize, squaresize * 2)) psprint(ret, "-%g 0 rlineto 0 %g rlineto closepath fill" % \ (squaresize * 2, squaresize * 2)) # Get back out of the centre section. psprint(ret, "grestore") # Draw the endpoint square in large in the middle. if v == 1: psprint(ret, "gsave") psprint(ret, "%g %g translate" % \ ((x + 0.5) * gridpitch, (h - y - 0.5) * gridpitch)) psprint(ret, "%g dup scale" % (float(gridpitch) / 2)) psprint(ret, "newpath %g %g moveto 0 -%g rlineto" % \ (squaresize, squaresize, squaresize * 2)) psprint(ret, "-%g 0 rlineto 0 %g rlineto closepath fill" % \ (squaresize * 2, squaresize * 2)) psprint(ret, "grestore") return ret.coords, ret.s def pattern_format(s): ret = Holder() ret.s = "" # Parse the game ID. params, seed = string.split(s, ":") w, h = map(string.atoi, string.split(params, "x")) rowdata = map(lambda s: string.split(s, "."), string.split(seed, "/")) assert len(rowdata) == w+h # I'm going to arbitrarily choose to use 7pt text for the # numbers, and a 14pt grid pitch. textht = 7 gridpitch = 14 gutter = 8 # between the numbers and the grid # Find the maximum number of numbers in each dimension, to # determine the border size required. xborder = reduce(max, map(len, rowdata[w:])) yborder = reduce(max, map(len, rowdata[:w])) # Set up coordinate system. I'm going to put the origin at the # _top left_ of the grid, so that both sets of numbers get # drawn the same way. pw = (w + xborder) * gridpitch + gutter ph = (h + yborder) * gridpitch + gutter ret.coords = (xborder * gridpitch + gutter, w * gridpitch, \ yborder * gridpitch + gutter, h * gridpitch) # Draw the internal grid lines. Every fifth one is thicker, as # a visual aid. psprint(ret, "newpath 0.1 setlinewidth") for x in xrange(1,w): if x % 5 != 0: psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, -h * gridpitch)) for y in xrange(1,h): if y % 5 != 0: psprint(ret, "0 %g moveto %g 0 rlineto" % (-y * gridpitch, w * gridpitch)) psprint(ret, "stroke") psprint(ret, "newpath 0.75 setlinewidth") for x in xrange(5,w,5): psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, -h * gridpitch)) for y in xrange(5,h,5): psprint(ret, "0 %g moveto %g 0 rlineto" % (-y * gridpitch, w * gridpitch)) psprint(ret, "stroke") # Draw round the grid exterior. psprint(ret, "newpath 1.5 setlinewidth") psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \ (-h * gridpitch, w * gridpitch, h * gridpitch)) psprint(ret, "closepath stroke") # And draw the numbers. psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht) for i in range(w+h): ns = rowdata[i] if i < w: xo = (i + 0.5) * gridpitch yo = (gutter + 0.5 * gridpitch) else: xo = -(gutter + 0.5 * gridpitch) yo = ((i-w) + 0.5) * -gridpitch for j in range(len(ns)-1, -1, -1): psprint(ret, "%g %g (%s) ctshow" % (xo, yo, ns[j])) if i < w: yo = yo + gridpitch else: xo = xo - gridpitch return ret.coords, ret.s def solo_format(s): ret = Holder() ret.s = "" # Parse the game ID. params, seed = string.split(s, ":") c, r = map(string.atoi, string.split(params, "x")) cr = c*r grid = [] while len(seed) > 0: if seed[0] in '_'+string.lowercase: if seed[0] in string.lowercase: grid.extend([-1] * (ord(seed[0]) - ord('a') + 1)) seed = seed[1:] elif seed[0] in string.digits: ns = "" while len(seed) > 0 and seed[0] in string.digits: ns = ns + seed[0] seed = seed[1:] grid.append(string.atoi(ns)) assert cr * cr == len(grid) # I'm going to arbitrarily choose to use 9pt text for the # numbers, and a 16pt grid pitch. textht = 9 gridpitch = 16 # Set up coordinate system. pw = ph = gridpitch * cr ret.coords = (pw/2, pw/2, ph/2, ph/2) psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2])) # Draw the thin internal grid lines. psprint(ret, "newpath 0.1 setlinewidth") for x in xrange(1,cr): if x % r != 0: psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, cr * gridpitch)) for y in xrange(1,cr): if y % c != 0: psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, cr * gridpitch)) psprint(ret, "stroke") # Draw the thicker internal grid lines. psprint(ret, "newpath 1 setlinewidth") for x in xrange(r,cr,r): psprint(ret, "%g 0 moveto 0 %g rlineto" % (x * gridpitch, cr * gridpitch)) for y in xrange(c,cr,c): psprint(ret, "0 %g moveto %g 0 rlineto" % (y * gridpitch, cr * gridpitch)) psprint(ret, "stroke") # Draw round the grid exterior, thicker still. psprint(ret, "newpath 1.5 setlinewidth") psprint(ret, "0 0 moveto 0 %g rlineto %g 0 rlineto 0 %g rlineto" % \ (cr * gridpitch, cr * gridpitch, -cr * gridpitch)) psprint(ret, "closepath stroke") # And draw the numbers. psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht) for y in xrange(cr): for x in xrange(cr): n = grid[y*cr+x] if n > 0: if n > 9: s = chr(ord('a') + n - 10) else: s = chr(ord('0') + n) psprint(ret, "%g %g (%s) ctshow" % \ ((x+0.5)*gridpitch, (cr-y-0.5)*gridpitch, s)) return ret.coords, ret.s def dominosa_format(s): ret = Holder() ret.s = "" params, seed = string.split(s, ":") n = string.atoi(params) w = n+2 h = n+1 grid = [] while len(seed) > 0: if seed[0] == '[': # XXX d, seed = string.split(seed[1:], "]") grid.append(string.atoi(d)) else: assert seed[0] in string.digits grid.append(string.atoi(seed[0:1])) seed = seed[1:] assert w*h == len(grid) # I'm going to arbitrarily choose to use 9pt text for the # numbers, and a 16pt grid pitch. textht = 9 gridpitch = 16 pw = gridpitch * w ph = gridpitch * h psprint(ret, "/Helvetica findfont %g scalefont setfont" % textht) ret.coords = (pw/2, pw/2, ph/2, ph/2) psprint(ret, "%g %g translate" % (-ret.coords[0], -ret.coords[2])) for y in xrange(h): for x in xrange(w): psprint(ret, "%g %g (%d) ctshow" % \ ((x+0.5)*gridpitch, (h-y-0.5)*gridpitch, grid[y*w+x])) return ret.coords, ret.s formatters = { "net": net_format, "rect": rect_format, "rectangles": rect_format, "pattern": pattern_format, "solo": solo_format, "dominosa": dominosa_format } if len(sys.argv) < 3: sys.stderr.write("print.py: expected two arguments (game and format)\n") sys.exit(1) formatter = formatters.get(sys.argv[1], None) if formatter == None: sys.stderr.write("print.py: unrecognised game name `%s'\n" % sys.argv[1]) sys.exit(1) try: format = map(string.atoi, string.split(sys.argv[2], "x")) except ValueError, e: format = [] if len(format) != 2: sys.stderr.write("print.py: expected format such as `2x3' as second" \ + " argument\n") sys.exit(1) xx, yy = format ppp = xx * yy # puzzles per page ids = [] while 1: s = sys.stdin.readline() if s == "": break if s[-1:] == "\n": s = s[:-1] ids.append(s) pages = int((len(ids) + ppp - 1) / ppp) # Output initial DSC stuff. print "%!PS-Adobe-3.0" print "%%Creator: print.py from Simon Tatham's Puzzle Collection" print "%%DocumentData: Clean7Bit" print "%%LanguageLevel: 1" print "%%Pages:", pages print "%%DocumentNeededResources:" print "%%+ font Helvetica" print "%%DocumentSuppliedResources: procset Puzzles 0 0" print "%%EndComments" print "%%BeginProlog" print "%%BeginResource: procset Puzzles 0 0" print "/ctshow {" print " 3 1 roll" print " newpath 0 0 moveto (X) true charpath flattenpath pathbbox" print " 3 -1 roll add 2 div 3 1 roll pop pop sub moveto" print " dup stringwidth pop 0.5 mul neg 0 rmoveto show" print "} bind def" print "%%EndResource" print "%%EndProlog" print "%%BeginSetup" print "%%IncludeResource: font Helvetica" print "%%EndSetup" # Now do each page. puzzle_index = 0; for i in xrange(1, pages+1): print "%%Page:", i, i print "save" # Do the drawing for each puzzle, giving a set of PS fragments # and bounding boxes. fragments = [['' for i in xrange(xx)] for i in xrange(yy)] lrbound = [(0,0) for i in xrange(xx)] tbbound = [(0,0) for i in xrange(yy)] for y in xrange(yy): for x in xrange(xx): if puzzle_index >= len(ids): break coords, frag = formatter(ids[puzzle_index]) fragments[y][x] = frag lb, rb = lrbound[x] lrbound[x] = (max(lb, coords[0]), max(rb, coords[1])) tb, bb = tbbound[y] tbbound[y] = (max(tb, coords[2]), max(bb, coords[3])) puzzle_index = puzzle_index + 1 # Now we know the sizes of everything, do the drawing in such a # way that we provide equal gutter space at the page edges and # between puzzle rows/columns. for y in xrange(yy): for x in xrange(xx): if len(fragments[y][x]) > 0: print "gsave" print "clippath flattenpath pathbbox pop pop translate" print "clippath flattenpath pathbbox 4 2 roll pop pop" # Compute the total height of all puzzles, which # we'll use it to work out the amount of gutter # space below this puzzle. htotal = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, tbbound), 0) # Now compute the total height of all puzzles # _below_ this one, plus the height-below-origin of # this one. hbelow = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, tbbound[y+1:]), 0) hbelow = hbelow + tbbound[y][1] print "%g sub %d mul %d div %g add exch" % (htotal, yy-y, yy+1, hbelow) # Now do all the same computations for width, # except we need the total width of everything # _before_ this one since the coordinates work the # other way round. wtotal = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, lrbound), 0) # Now compute the total height of all puzzles # _below_ this one, plus the height-below-origin of # this one. wleft = reduce(lambda a,b:a+b, map(lambda (a,b):a+b, lrbound[:x]), 0) wleft = wleft + lrbound[x][0] print "%g sub %d mul %d div %g add exch" % (wtotal, x+1, xx+1, wleft) print "translate" sys.stdout.write(fragments[y][x]) print "grestore" print "restore showpage" print "%%EOF"