Added a `--generate' command-line option in the GTK port of every
authorsimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Fri, 29 Apr 2005 17:07:19 +0000 (17:07 +0000)
committersimon <simon@cda61777-01e9-0310-a592-d414129be87e>
Fri, 29 Apr 2005 17:07:19 +0000 (17:07 +0000)
puzzle, to make it construcct puzzle IDs and output them on stdout.
Also checked in print.py, a script which reads puzzle IDs on stdin
and produces PostScript output. With these, you can generate pages
of Pattern, Rectangles and Solo puzzles to take on trains with you.

git-svn-id: svn://svn.tartarus.org/sgt/puzzles@5707 cda61777-01e9-0310-a592-d414129be87e

gtk.c
print.py [new file with mode: 0755]

diff --git a/gtk.c b/gtk.c
index 5690eae..93a81d2 100644 (file)
--- a/gtk.c
+++ b/gtk.c
@@ -1031,14 +1031,70 @@ int main(int argc, char **argv)
     char *pname = argv[0];
     char *error;
 
-    gtk_init(&argc, &argv);
+    /*
+     * Special standalone mode for generating puzzle IDs on the
+     * command line. Useful for generating puzzles to be printed
+     * out and solved offline (for puzzles where that even makes
+     * sense - Solo, for example, is a lot more pencil-and-paper
+     * friendly than Net!)
+     * 
+     * Usage:
+     * 
+     *   <puzzle-name> --generate [<n> [<params>]]
+     * 
+     * <n>, if present, is the number of puzzle IDs to generate.
+     * <params>, if present, is the same type of parameter string
+     * you would pass to the puzzle when running it in GUI mode,
+     * including optional extras such as the expansion factor in
+     * Rectangles and the difficulty level in Solo.
+     * 
+     * If you specify <params>, you must also specify <n> (although
+     * you may specify it to be 1). Sorry; that was the
+     * simplest-to-parse command-line syntax I came up with.
+     */
+    if (argc > 1 && !strcmp(argv[1], "--generate")) {
+       int n = 1;
+       char *params = NULL;
+       game_params *par;
+       random_state *rs;
+       char *parstr;
+
+       {
+           void *seed;
+           int seedlen;
+           get_random_seed(&seed, &seedlen);
+           rs = random_init(seed, seedlen);
+       }
 
-    if (!new_window(argc > 1 ? argv[1] : NULL, &error)) {
-        fprintf(stderr, "%s: %s\n", pname, error);
-        return 1;
-    }
+       if (argc > 2)
+           n = atoi(argv[2]);
+       if (argc > 3)
+           params = argv[3];
+
+       if (params)
+           par = thegame.decode_params(params);
+       else
+           par = thegame.default_params();
+       parstr = thegame.encode_params(par);
+
+       while (n-- > 0) {
+           char *seed = thegame.new_seed(par, rs);
+           printf("%s:%s\n", parstr, seed);
+           sfree(seed);
+       }
 
-    gtk_main();
+       return 0;
+    } else {
+
+       gtk_init(&argc, &argv);
+
+       if (!new_window(argc > 1 ? argv[1] : NULL, &error)) {
+           fprintf(stderr, "%s: %s\n", pname, error);
+           return 1;
+       }
+
+       gtk_main();
+    }
 
     return 0;
 }
diff --git a/print.py b/print.py
new file mode 100755 (executable)
index 0000000..172ae51
--- /dev/null
+++ b/print.py
@@ -0,0 +1,343 @@
+#!/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 and Solo.
+
+# Command-line syntax is
+#
+#     print.py <game-name> <format>
+#
+# <game-name> is one of `rect', `rectangles', `pattern', `solo'.
+# <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 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
+
+formatters = {
+"rect": rect_format,
+"rectangles": rect_format,
+"pattern": pattern_format,
+"solo": solo_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"