Remove debug output.
[sgt/puzzles] / mines.c
diff --git a/mines.c b/mines.c
index 42543cc..0141517 100644 (file)
--- a/mines.c
+++ b/mines.c
@@ -2,13 +2,6 @@
  * mines.c: Minesweeper clone with sophisticated grid generation.
  * 
  * Still TODO:
- * 
- *  - possibly disable undo? Or alternatively mark game states as
- *    `cheated', although that's horrid.
- *     + OK. Rather than _disabling_ undo, we have a hook callable
- *       in the game backend which is called before we do an undo.
- *       That hook can talk to the game_ui and set the cheated flag,
- *       and then make_move can avoid setting the `won' flag after that.
  *
  *  - think about configurably supporting question marks. Once,
  *    that is, we've thought about configurability in general!
@@ -32,10 +25,11 @@ enum {
     NCOLOURS
 };
 
-#define TILE_SIZE 20
+#define PREFERRED_TILE_SIZE 20
+#define TILE_SIZE (ds->tilesize)
 #define BORDER (TILE_SIZE * 3 / 2)
-#define HIGHLIGHT_WIDTH 2
-#define OUTER_HIGHLIGHT_WIDTH 3
+#define HIGHLIGHT_WIDTH (TILE_SIZE / 10)
+#define OUTER_HIGHLIGHT_WIDTH (BORDER / 10)
 #define COORD(x)  ( (x) * TILE_SIZE + BORDER )
 #define FROMCOORD(x)  ( ((x) - BORDER + TILE_SIZE) / TILE_SIZE - 1 )
 
@@ -102,24 +96,25 @@ static game_params *default_params(void)
     return ret;
 }
 
+static const struct game_params mines_presets[] = {
+  {9, 9, 10, TRUE},
+  {9, 9, 35, TRUE},
+  {16, 16, 40, TRUE},
+  {16, 16, 99, TRUE},
+  {30, 16, 99, TRUE},
+  {30, 16, 170, TRUE},
+};
+
 static int game_fetch_preset(int i, char **name, game_params **params)
 {
     game_params *ret;
     char str[80];
-    static const struct { int w, h, n; } values[] = {
-        {9, 9, 10},
-        {16, 16, 40},
-        {30, 16, 99},
-    };
 
-    if (i < 0 || i >= lenof(values))
+    if (i < 0 || i >= lenof(mines_presets))
         return FALSE;
 
     ret = snew(game_params);
-    ret->w = values[i].w;
-    ret->h = values[i].h;
-    ret->n = values[i].n;
-    ret->unique = TRUE;
+    *ret = mines_presets[i];
 
     sprintf(str, "%dx%d, %d mines", ret->w, ret->h, ret->n);
 
@@ -244,12 +239,22 @@ static game_params *custom_params(config_item *cfg)
 
 static char *validate_params(game_params *params)
 {
-    if (params->w <= 0 && params->h <= 0)
-       return "Width and height must both be greater than zero";
-    if (params->w <= 0)
-       return "Width must be greater than zero";
-    if (params->h <= 0)
-       return "Height must be greater than zero";
+    /*
+     * Lower limit on grid size: each dimension must be at least 3.
+     * 1 is theoretically workable if rather boring, but 2 is a
+     * real problem: there is often _no_ way to generate a uniquely
+     * solvable 2xn Mines grid. You either run into two mines
+     * blocking the way and no idea what's behind them, or one mine
+     * and no way to know which of the two rows it's in. If the
+     * mine count is even you can create a soluble grid by packing
+     * all the mines at one end (so what when you hit a two-mine
+     * wall there are only as many covered squares left as there
+     * are mines); but if it's odd, you are doomed, because you
+     * _have_ to have a gap somewhere which you can't determine the
+     * position of.
+     */
+    if (params->w <= 2 || params->h <= 2)
+       return "Width and height must both be greater than two";
     if (params->n > params->w * params->h - 9)
        return "Too many mines for grid size";
 
@@ -555,9 +560,11 @@ static void std_add(struct squaretodo *std, int i)
     std->next[i] = -1;
 }
 
+typedef int (*open_cb)(void *, int, int);
+
 static void known_squares(int w, int h, struct squaretodo *std,
-                         signed char *grid,
-                         int (*open)(void *ctx, int x, int y), void *openctx,
+                          signed char *grid,
+                         open_cb open, void *openctx,
                          int x, int y, int mask, int mine)
 {
     int xx, yy, bit;
@@ -623,11 +630,12 @@ struct perturbations {
  *    steps were required; the exact return value is the number of
  *    perturb calls.
  */
+
+typedef struct perturbations *(*perturb_cb) (void *, signed char *, int, int, int);
+
 static int minesolve(int w, int h, int n, signed char *grid,
-                    int (*open)(void *ctx, int x, int y),
-                    struct perturbations *(*perturb)(void *ctx,
-                                                     signed char *grid,
-                                                     int x, int y, int mask),
+                    open_cb open,
+                     perturb_cb perturb,
                     void *ctx, random_state *rs)
 {
     struct setstore *ss = ss_new();
@@ -1287,7 +1295,7 @@ static int minesolve(int w, int h, int n, signed char *grid,
  */
 
 struct minectx {
-    signed char *grid;
+    char *grid;
     int w, h;
     int sx, sy;
     int allow_big_perturbs;
@@ -1782,7 +1790,7 @@ static char *minegen(int w, int h, int n, int x, int y, int unique,
         * We bypass this bit if we're not after a unique grid.
          */
        if (unique) {
-           signed char *solvegrid = snewn(w*h, char);
+           signed char *solvegrid = snewn(w*h, signed char);
            struct minectx actx, *ctx = &actx;
            int solveret, prevret = -2;
 
@@ -1926,12 +1934,51 @@ static void obfuscate_bitmap(unsigned char *bmp, int bits, int decode)
     }
 }
 
+static char *describe_layout(char *grid, int area, int x, int y,
+                             int obfuscate)
+{
+    char *ret, *p;
+    unsigned char *bmp;
+    int i;
+
+    /*
+     * Set up the mine bitmap and obfuscate it.
+     */
+    bmp = snewn((area + 7) / 8, unsigned char);
+    memset(bmp, 0, (area + 7) / 8);
+    for (i = 0; i < area; i++) {
+        if (grid[i])
+            bmp[i / 8] |= 0x80 >> (i % 8);
+    }
+    if (obfuscate)
+        obfuscate_bitmap(bmp, area, FALSE);
+
+    /*
+     * Now encode the resulting bitmap in hex. We can work to
+     * nibble rather than byte granularity, since the obfuscation
+     * function guarantees to return a bit string of the same
+     * length as its input.
+     */
+    ret = snewn((area+3)/4 + 100, char);
+    p = ret + sprintf(ret, "%d,%d,%s", x, y,
+                      obfuscate ? "m" : "");   /* 'm' == masked */
+    for (i = 0; i < (area+3)/4; i++) {
+        int v = bmp[i/2];
+        if (i % 2 == 0)
+            v >>= 4;
+        *p++ = "0123456789abcdef"[v & 0xF];
+    }
+    *p = '\0';
+
+    sfree(bmp);
+
+    return ret;
+}
+
 static char *new_mine_layout(int w, int h, int n, int x, int y, int unique,
                             random_state *rs, char **game_desc)
 {
-    signed char *grid, *ret, *p;
-    unsigned char *bmp;
-    int i, area;
+    char *grid;
 
 #ifdef TEST_OBFUSCATION
     static int tested_obfuscation = FALSE;
@@ -1998,39 +2045,8 @@ static char *new_mine_layout(int w, int h, int n, int x, int y, int unique,
 
     grid = minegen(w, h, n, x, y, unique, rs);
 
-    if (game_desc) {
-       /*
-        * Set up the mine bitmap and obfuscate it.
-        */
-       area = w * h;
-       bmp = snewn((area + 7) / 8, unsigned char);
-       memset(bmp, 0, (area + 7) / 8);
-       for (i = 0; i < area; i++) {
-           if (grid[i])
-               bmp[i / 8] |= 0x80 >> (i % 8);
-       }
-       obfuscate_bitmap(bmp, area, FALSE);
-
-       /*
-        * Now encode the resulting bitmap in hex. We can work to
-        * nibble rather than byte granularity, since the obfuscation
-        * function guarantees to return a bit string of the same
-        * length as its input.
-        */
-       ret = snewn((area+3)/4 + 100, char);
-       p = ret + sprintf(ret, "%d,%d,m", x, y);   /* 'm' == masked */
-       for (i = 0; i < (area+3)/4; i++) {
-           int v = bmp[i/2];
-           if (i % 2 == 0)
-               v >>= 4;
-           *p++ = "0123456789abcdef"[v & 0xF];
-       }
-       *p = '\0';
-
-       sfree(bmp);
-
-       *game_desc = ret;
-    }  
+    if (game_desc)
+        *game_desc = describe_layout(grid, w * h, x, y, TRUE);
 
     return grid;
 }
@@ -2038,13 +2054,25 @@ static char *new_mine_layout(int w, int h, int n, int x, int y, int unique,
 static char *new_game_desc(game_params *params, random_state *rs,
                           game_aux_info **aux, int interactive)
 {
+    /*
+     * We generate the coordinates of an initial click even if they
+     * aren't actually used. This has the effect of harmonising the
+     * random number usage between interactive and batch use: if
+     * you use `mines --generate' with an explicit random seed, you
+     * should get exactly the same results as if you type the same
+     * random seed into the interactive game and click in the same
+     * initial location. (Of course you won't get the same grid if
+     * you click in a _different_ initial location, but there's
+     * nothing to be done about that.)
+     */
+    int x = random_upto(rs, params->w);
+    int y = random_upto(rs, params->h);
+
     if (!interactive) {
        /*
         * For batch-generated grids, pre-open one square.
         */
-       int x = random_upto(rs, params->w);
-       int y = random_upto(rs, params->h);
-       signed char *grid;
+       char *grid;
        char *desc;
 
        grid = new_mine_layout(params->w, params->h, params->n,
@@ -2056,7 +2084,7 @@ static char *new_game_desc(game_params *params, random_state *rs,
 
        rsdesc = random_state_encode(rs);
        desc = snewn(strlen(rsdesc) + 100, char);
-       sprintf(desc, "r%d,%c,%s", params->n, params->unique ? 'u' : 'a', rsdesc);
+       sprintf(desc, "r%d,%c,%s", params->n, (char)(params->unique ? 'u' : 'a'), rsdesc);
        sfree(rsdesc);
        return desc;
     }
@@ -2142,22 +2170,11 @@ static int open_square(game_state *state, int x, int y)
 
     if (state->layout->mines[y*w+x]) {
        /*
-        * The player has landed on a mine. Bad luck. Expose all
-        * the mines.
+        * The player has landed on a mine. Bad luck. Expose the
+        * mine that killed them, but not the rest (in case they
+        * want to Undo and carry on playing).
         */
        state->dead = TRUE;
-       for (yy = 0; yy < h; yy++)
-           for (xx = 0; xx < w; xx++) {
-               if (state->layout->mines[yy*w+xx] &&
-                   (state->grid[yy*w+xx] == -2 ||
-                    state->grid[yy*w+xx] == -3)) {
-                   state->grid[yy*w+xx] = 64;
-               }
-               if (!state->layout->mines[yy*w+xx] &&
-                   state->grid[yy*w+xx] == -1) {
-                   state->grid[yy*w+xx] = 66;
-               }
-           }
        state->grid[y*w+x] = 65;
        return -1;
     }
@@ -2254,9 +2271,10 @@ static game_state *new_game(midend_data *me, game_params *params, char *desc)
     wh = state->w * state->h;
 
     state->layout = snew(struct mine_layout);
+    memset(state->layout, 0, sizeof(struct mine_layout));
     state->layout->refcount = 1;
 
-    state->grid = snewn(wh, char);
+    state->grid = snewn(wh, signed char);
     memset(state->grid, -2, wh);
 
     if (*desc == 'r') {
@@ -2330,6 +2348,7 @@ static game_state *new_game(midend_data *me, game_params *params, char *desc)
        }
 
        ret = open_square(state, x, y);
+        sfree(bmp);
     }
 
     return state;
@@ -2348,7 +2367,7 @@ static game_state *dup_game(game_state *state)
     ret->just_used_solve = state->just_used_solve;
     ret->layout = state->layout;
     ret->layout->refcount++;
-    ret->grid = snewn(ret->w * ret->h, char);
+    ret->grid = snewn(ret->w * ret->h, signed char);
     memcpy(ret->grid, state->grid, ret->w * ret->h);
 
     return ret;
@@ -2439,6 +2458,7 @@ static char *game_text_format(game_state *state)
 struct game_ui {
     int hx, hy, hradius;              /* for mouse-down highlights */
     int flash_is_death;
+    int deaths;
 };
 
 static game_ui *new_ui(game_state *state)
@@ -2446,6 +2466,7 @@ static game_ui *new_ui(game_state *state)
     game_ui *ui = snew(game_ui);
     ui->hx = ui->hy = -1;
     ui->hradius = 0;
+    ui->deaths = 0;
     ui->flash_is_death = FALSE;               /* *shrug* */
     return ui;
 }
@@ -2455,6 +2476,26 @@ static void free_ui(game_ui *ui)
     sfree(ui);
 }
 
+static void game_changed_state(game_ui *ui, game_state *oldstate,
+                               game_state *newstate)
+{
+}
+
+struct game_drawstate {
+    int w, h, started, tilesize;
+    signed char *grid;
+    /*
+     * Items in this `grid' array have all the same values as in
+     * the game_state grid, and in addition:
+     * 
+     *         - -10 means the tile was drawn `specially' as a result of a
+     *           flash, so it will always need redrawing.
+     * 
+     *         - -22 and -23 mean the tile is highlighted for a possible
+     *           click.
+     */
+};
+
 static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
                              int x, int y, int button)
 {
@@ -2470,10 +2511,12 @@ static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
 
     cx = FROMCOORD(x);
     cy = FROMCOORD(y);
-    if (cx < 0 || cx >= from->w || cy < 0 || cy > from->h)
-       return NULL;
 
-    if (button == LEFT_BUTTON || button == LEFT_DRAG) {
+    if (button == LEFT_BUTTON || button == LEFT_DRAG ||
+       button == MIDDLE_BUTTON || button == MIDDLE_DRAG) {
+       if (cx < 0 || cx >= from->w || cy < 0 || cy >= from->h)
+           return NULL;
+
        /*
         * Mouse-downs and mouse-drags just cause highlighting
         * updates.
@@ -2485,6 +2528,9 @@ static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
     }
 
     if (button == RIGHT_BUTTON) {
+       if (cx < 0 || cx >= from->w || cy < 0 || cy >= from->h)
+           return NULL;
+
        /*
         * Right-clicking only works on a covered square, and it
         * toggles between -1 (marked as mine) and -2 (not marked
@@ -2503,7 +2549,7 @@ static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
        return ret;
     }
 
-    if (button == LEFT_RELEASE) {
+    if (button == LEFT_RELEASE || button == MIDDLE_RELEASE) {
        ui->hx = ui->hy = -1;
        ui->hradius = 0;
 
@@ -2511,25 +2557,30 @@ static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
         * At this stage we must never return NULL: we have adjusted
         * the ui, so at worst we return `from'.
         */
+       if (cx < 0 || cx >= from->w || cy < 0 || cy >= from->h)
+           return from;
 
        /*
         * Left-clicking on a covered square opens a tile. Not
         * permitted if the tile is marked as a mine, for safety.
         * (Unmark it and _then_ open it.)
         */
-       if (from->grid[cy * from->w + cx] == -2 ||
-           from->grid[cy * from->w + cx] == -3) {
+       if (button == LEFT_RELEASE &&
+           (from->grid[cy * from->w + cx] == -2 ||
+            from->grid[cy * from->w + cx] == -3)) {
            ret = dup_game(from);
             ret->just_used_solve = FALSE;
            open_square(ret, cx, cy);
+            if (ret->dead)
+                ui->deaths++;
            return ret;
        }
 
        /*
-        * Left-clicking on an uncovered tile: first we check to see if
-        * the number of mine markers surrounding the tile is equal to
-        * its mine count, and if so then we open all other surrounding
-        * squares.
+        * Left-clicking or middle-clicking on an uncovered tile:
+        * first we check to see if the number of mine markers
+        * surrounding the tile is equal to its mine count, and if
+        * so then we open all other surrounding squares.
         */
        if (from->grid[cy * from->w + cx] > 0) {
            int dy, dx, n;
@@ -2554,6 +2605,8 @@ static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
                            (ret->grid[(cy+dy)*ret->w+(cx+dx)] == -2 ||
                             ret->grid[(cy+dy)*ret->w+(cx+dx)] == -3))
                            open_square(ret, cx+dx, cy+dy);
+                if (ret->dead)
+                    ui->deaths++;
                return ret;
            }
        }
@@ -2568,23 +2621,23 @@ static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
  * Drawing routines.
  */
 
-struct game_drawstate {
-    int w, h, started;
-    signed char *grid;
+static void game_size(game_params *params, game_drawstate *ds,
+                      int *x, int *y, int expand)
+{
+    int tsx, tsy, ts;
     /*
-     * Items in this `grid' array have all the same values as in
-     * the game_state grid, and in addition:
-     * 
-     *         - -10 means the tile was drawn `specially' as a result of a
-     *           flash, so it will always need redrawing.
-     * 
-     *         - -22 and -23 mean the tile is highlighted for a possible
-     *           click.
+     * Each window dimension equals the tile size times 3 more than
+     * the grid dimension (the border is 3/2 the width of the
+     * tiles).
      */
-};
+    tsx = *x / (params->w + 3);
+    tsy = *y / (params->h + 3);
+    ts = min(tsx, tsy);
+    if (expand)
+        ds->tilesize = ts;
+    else
+        ds->tilesize = min(ts, PREFERRED_TILE_SIZE);
 
-static void game_size(game_params *params, int *x, int *y)
-{
     *x = BORDER * 2 + TILE_SIZE * params->w;
     *y = BORDER * 2 + TILE_SIZE * params->h;
 }
@@ -2674,7 +2727,8 @@ static game_drawstate *game_new_drawstate(game_state *state)
     ds->w = state->w;
     ds->h = state->h;
     ds->started = FALSE;
-    ds->grid = snewn(ds->w * ds->h, char);
+    ds->tilesize = 0;                  /* not decided yet */
+    ds->grid = snewn(ds->w * ds->h, signed char);
 
     memset(ds->grid, -99, ds->w * ds->h);
 
@@ -2687,7 +2741,8 @@ static void game_free_drawstate(game_drawstate *ds)
     sfree(ds);
 }
 
-static void draw_tile(frontend *fe, int x, int y, int v, int bg)
+static void draw_tile(frontend *fe, game_drawstate *ds,
+                      int x, int y, int v, int bg)
 {
     if (v < 0) {
         int coords[12];
@@ -2869,7 +2924,7 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate,
        bg = COL_BACKGROUND;
 
     if (!ds->started) {
-        int coords[6];
+        int coords[10];
 
        draw_rect(fe, 0, 0,
                  TILE_SIZE * state->w + 2 * BORDER,
@@ -2885,15 +2940,19 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate,
         coords[1] = COORD(state->h) + OUTER_HIGHLIGHT_WIDTH - 1;
         coords[2] = COORD(state->w) + OUTER_HIGHLIGHT_WIDTH - 1;
         coords[3] = COORD(0) - OUTER_HIGHLIGHT_WIDTH;
-        coords[4] = COORD(0) - OUTER_HIGHLIGHT_WIDTH;
-        coords[5] = COORD(state->h) + OUTER_HIGHLIGHT_WIDTH - 1;
-        draw_polygon(fe, coords, 3, TRUE, COL_HIGHLIGHT);
-        draw_polygon(fe, coords, 3, FALSE, COL_HIGHLIGHT);
+        coords[4] = coords[2] - TILE_SIZE;
+        coords[5] = coords[3] + TILE_SIZE;
+        coords[8] = COORD(0) - OUTER_HIGHLIGHT_WIDTH;
+        coords[9] = COORD(state->h) + OUTER_HIGHLIGHT_WIDTH - 1;
+        coords[6] = coords[8] + TILE_SIZE;
+        coords[7] = coords[9] - TILE_SIZE;
+        draw_polygon(fe, coords, 5, TRUE, COL_HIGHLIGHT);
+        draw_polygon(fe, coords, 5, FALSE, COL_HIGHLIGHT);
 
         coords[1] = COORD(0) - OUTER_HIGHLIGHT_WIDTH;
         coords[0] = COORD(0) - OUTER_HIGHLIGHT_WIDTH;
-        draw_polygon(fe, coords, 3, TRUE, COL_LOWLIGHT);
-        draw_polygon(fe, coords, 3, FALSE, COL_LOWLIGHT);
+        draw_polygon(fe, coords, 5, TRUE, COL_LOWLIGHT);
+        draw_polygon(fe, coords, 5, FALSE, COL_LOWLIGHT);
 
         ds->started = TRUE;
     }
@@ -2917,7 +2976,7 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate,
                v -= 20;
 
            if (ds->grid[y*ds->w+x] != v || bg != COL_BACKGROUND) {
-               draw_tile(fe, COORD(x), COORD(y), v, bg);
+               draw_tile(fe, ds, COORD(x), COORD(y), v, bg);
                ds->grid[y*ds->w+x] = (bg == COL_BACKGROUND ? v : -10);
            }
        }
@@ -2931,15 +2990,18 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate,
     {
        char statusbar[512];
        if (state->dead) {
-           sprintf(statusbar, "GAME OVER!");
+           sprintf(statusbar, "DEAD!");
        } else if (state->won) {
             if (state->used_solve)
                 sprintf(statusbar, "Auto-solved.");
             else
                 sprintf(statusbar, "COMPLETED!");
        } else {
-           sprintf(statusbar, "Mines marked: %d / %d", markers, mines);
+           sprintf(statusbar, "Marked: %d / %d", markers, mines);
        }
+        if (ui->deaths)
+            sprintf(statusbar + strlen(statusbar),
+                    "  Deaths: %d", ui->deaths);
        status_bar(fe, statusbar);
     }
 }
@@ -3005,6 +3067,7 @@ const struct game thegame = {
     TRUE, game_text_format,
     new_ui,
     free_ui,
+    game_changed_state,
     make_move,
     game_size,
     game_colours,
@@ -3015,4 +3078,110 @@ const struct game thegame = {
     game_flash_length,
     game_wants_statusbar,
     TRUE, game_timing_state,
+    BUTTON_BEATS(LEFT_BUTTON, RIGHT_BUTTON),
 };
+
+#ifdef STANDALONE_OBFUSCATOR
+
+/*
+ * Vaguely useful stand-alone program which translates between
+ * obfuscated and clear Mines game descriptions. Pass in a game
+ * description on the command line, and if it's clear it will be
+ * obfuscated and vice versa. The output text should also be a
+ * valid game ID describing the same game. Like this:
+ *
+ * $ ./mineobfusc 9x9:4,4,mb071b49fbd1cb6a0d5868
+ * 9x9:4,4,004000007c00010022080
+ * $ ./mineobfusc 9x9:4,4,004000007c00010022080
+ * 9x9:4,4,mb071b49fbd1cb6a0d5868
+ *
+ * gcc -DSTANDALONE_OBFUSCATOR -o mineobfusc mines.c malloc.c random.c tree234.c
+ */
+
+#include <stdarg.h>
+
+void frontend_default_colour(frontend *fe, float *output) {}
+void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize,
+               int align, int colour, char *text) {}
+void draw_rect(frontend *fe, int x, int y, int w, int h, int colour) {}
+void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour) {}
+void draw_polygon(frontend *fe, int *coords, int npoints,
+                  int fill, int colour) {}
+void clip(frontend *fe, int x, int y, int w, int h) {}
+void unclip(frontend *fe) {}
+void start_draw(frontend *fe) {}
+void draw_update(frontend *fe, int x, int y, int w, int h) {}
+void end_draw(frontend *fe) {}
+void midend_supersede_game_desc(midend_data *me, char *desc) {}
+void status_bar(frontend *fe, char *text) {}
+
+void fatal(char *fmt, ...)
+{
+    va_list ap;
+
+    fprintf(stderr, "fatal error: ");
+
+    va_start(ap, fmt);
+    vfprintf(stderr, fmt, ap);
+    va_end(ap);
+
+    fprintf(stderr, "\n");
+    exit(1);
+}
+
+int main(int argc, char **argv)
+{
+    game_params *p;
+    game_state *s;
+    int recurse = TRUE;
+    char *id = NULL, *desc, *err;
+    int y, x;
+    int grade = FALSE;
+
+    while (--argc > 0) {
+        char *p = *++argv;
+       if (*p == '-') {
+            fprintf(stderr, "%s: unrecognised option `%s'\n", argv[0]);
+            return 1;
+        } else {
+            id = p;
+        }
+    }
+
+    if (!id) {
+        fprintf(stderr, "usage: %s <game_id>\n", argv[0]);
+        return 1;
+    }
+
+    desc = strchr(id, ':');
+    if (!desc) {
+        fprintf(stderr, "%s: game id expects a colon in it\n", argv[0]);
+        return 1;
+    }
+    *desc++ = '\0';
+
+    p = default_params();
+    decode_params(p, id);
+    err = validate_desc(p, desc);
+    if (err) {
+        fprintf(stderr, "%s: %s\n", argv[0], err);
+        return 1;
+    }
+    s = new_game(NULL, p, desc);
+
+    x = atoi(desc);
+    while (*desc && *desc != ',') desc++;
+    if (*desc) desc++;
+    y = atoi(desc);
+    while (*desc && *desc != ',') desc++;
+    if (*desc) desc++;
+
+    printf("%s:%s\n", id, describe_layout(s->layout->mines,
+                                          p->w * p->h,
+                                          x, y,
+                                          (*desc != 'm')));
+
+    return 0;
+}
+
+#endif