shithub: puzzles

Download patch

ref: 0d1a1f08bac25a4641c38a8e42dfa6e2bb9981d7
parent: a4fca3286f3aa630a3641e50a8e1f44ab1504a29
author: Simon Tatham <anakin@pobox.com>
date: Fri Apr 21 11:41:18 EDT 2023

Move per-puzzle ad-hoc getenv preferences into game_ui.

Environment variables that set specific settings of particular
puzzles, such as SLANT_SWAP_BUTTONS, LIGHTUP_LIT_BLOBS and
LOOPY_AUTOFOLLOW, now all affect the game behaviour via fields in
game_ui instead of being looked up by getenv in the individual
functions that need to know them.

The purpose of this refactoring is to put those config fields in a
place where other more user-friendly configuration systems will also
be able to access them, once I introduce one. However, for the moment,
there's no functional change: I haven't _yet_ changed how the user
sets those options. They're still set by environment variables alone.
All I'm changing here is where the settings are stored inside the
code, and exactly when they're read out of the environment to put into
the game_ui.

Specifically, the getenvs now happen during new_ui(). Or rather, in
all the puzzles I've changed here, they happen in a subroutine
legacy_prefs_override() called from within new_ui(), after it's set up
the default values for the settings, and then gives the environment a
chance to override them. Or rather, legacy_prefs_override() only
actually calls getenv the first time, and after that, it's cached the
answers it got.

In order to make the override functions less wordy, I've altered the
prototype of getenv_bool so that it returns an int rather than a bool,
and takes its default return value in the same form. That way you can
set the default to something other than 0 or 1, and find out whether a
value was present at all.

This commit only touches environment configuration specific to an
individual puzzle. The midend also has some standard environment-based
config options that apply to all puzzles, such as colour scheme and
default presets and preset-menu extension. I haven't handled those
yet.

--- a/fifteen.c
+++ b/fifteen.c
@@ -449,13 +449,44 @@
     return ret;
 }
 
+struct game_ui {
+    /*
+     * User-preference option: invert the direction of arrow-key
+     * control, so that the arrow on the key you press indicates in
+     * which direction you want the _space_ to move, rather than in
+     * which direction you want a tile to move to fill the space.
+     */
+    bool invert_cursor;
+};
+
+static void legacy_prefs_override(struct game_ui *ui_out)
+{
+    static bool initialised = false;
+    static int invert_cursor = -1;
+
+    if (!initialised) {
+        initialised = true;
+        invert_cursor = getenv_bool("FIFTEEN_INVERT_CURSOR", -1);
+    }
+
+    if (invert_cursor != -1)
+        ui_out->invert_cursor = invert_cursor;
+}
+
 static game_ui *new_ui(const game_state *state)
 {
-    return NULL;
+    struct game_ui *ui = snew(struct game_ui);
+
+    ui->invert_cursor = false;
+
+    legacy_prefs_override(ui);
+
+    return ui;
 }
 
 static void free_ui(game_ui *ui)
 {
+    sfree(ui);
 }
 
 static void game_changed_state(game_ui *ui, const game_state *oldstate,
@@ -708,11 +739,8 @@
         if (nx < 0 || nx >= state->w || ny < 0 || ny >= state->h)
             return NULL;               /* out of bounds */
     } else if (IS_CURSOR_MOVE(button)) {
-        static int invert_cursor = -1;
-        if (invert_cursor == -1)
-            invert_cursor = getenv_bool("FIFTEEN_INVERT_CURSOR", false);
         button = flip_cursor(button); /* the default */
-        if (invert_cursor)
+        if (ui->invert_cursor)
             button = flip_cursor(button); /* undoes the first flip */
 	move_cursor(button, &nx, &ny, state->w, state->h, false);
     } else if ((button == 'h' || button == 'H') && !state->completed) {
--- a/lightup.c
+++ b/lightup.c
@@ -1833,13 +1833,37 @@
 struct game_ui {
     int cur_x, cur_y;
     bool cur_visible;
+
+    /*
+     * User preference: when a square contains both a black blob for
+     * 'user is convinced this isn't a light' and a yellow highlight
+     * for 'this square is lit by a light', both of which rule out it
+     * being a light, should we still bother to show the blob?
+     */
+    bool draw_blobs_when_lit;
 };
 
+static void legacy_prefs_override(struct game_ui *ui_out)
+{
+    static bool initialised = false;
+    static int draw_blobs_when_lit = -1;
+
+    if (!initialised) {
+        initialised = true;
+        draw_blobs_when_lit = getenv_bool("LIGHTUP_LIT_BLOBS", -1);
+    }
+
+    if (draw_blobs_when_lit != -1)
+        ui_out->draw_blobs_when_lit = draw_blobs_when_lit;
+}
+
 static game_ui *new_ui(const game_state *state)
 {
     game_ui *ui = snew(game_ui);
     ui->cur_x = ui->cur_y = 0;
     ui->cur_visible = getenv_bool("PUZZLES_SHOW_CURSOR", false);
+    ui->draw_blobs_when_lit = true;
+    legacy_prefs_override(ui);
     return ui;
 }
 
@@ -2130,7 +2154,7 @@
     return ret;
 }
 
-static void tile_redraw(drawing *dr, game_drawstate *ds,
+static void tile_redraw(drawing *dr, game_drawstate *ds, const game_ui *ui,
                         const game_state *state, int x, int y)
 {
     unsigned int ds_flags = GRID(ds, flags, x, y);
@@ -2160,10 +2184,7 @@
             draw_circle(dr, dx + TILE_SIZE/2, dy + TILE_SIZE/2, TILE_RADIUS,
                         lcol, COL_BLACK);
         } else if ((ds_flags & DF_IMPOSSIBLE)) {
-            static int draw_blobs_when_lit = -1;
-            if (draw_blobs_when_lit < 0)
-		draw_blobs_when_lit = getenv_bool("LIGHTUP_LIT_BLOBS", true);
-            if (!(ds_flags & DF_LIT) || draw_blobs_when_lit) {
+            if (!(ds_flags & DF_LIT) || ui->draw_blobs_when_lit) {
                 int rlen = TILE_SIZE / 4;
                 draw_rect(dr, dx + TILE_SIZE/2 - rlen/2,
                           dy + TILE_SIZE/2 - rlen/2,
@@ -2208,7 +2229,7 @@
             unsigned int ds_flags = tile_flags(ds, state, ui, x, y, flashing);
             if (ds_flags != GRID(ds, flags, x, y)) {
                 GRID(ds, flags, x, y) = ds_flags;
-                tile_redraw(dr, ds, state, x, y);
+                tile_redraw(dr, ds, ui, state, x, y);
             }
         }
     }
--- a/loopy.c
+++ b/loopy.c
@@ -868,13 +868,67 @@
     return ret;
 }
 
+struct game_ui {
+    /*
+     * User preference: should grid lines in LINE_NO state be drawn
+     * very faintly so users can still see where they are, or should
+     * they be completely invisible?
+     */
+    bool draw_faint_lines;
+
+    /*
+     * User preference: when clicking an edge that has only one
+     * possible edge connecting to one (or both) of its ends, should
+     * that edge also change to the same state as the edge we just
+     * clicked?
+     */
+    enum {
+        AF_OFF,     /* no, all grid edges are independent in the UI */
+        AF_FIXED,   /* yes, but only based on the grid itself */
+        AF_ADAPTIVE /* yes, and consider edges user has already set to NO */
+    } autofollow;
+};
+
+static void legacy_prefs_override(struct game_ui *ui_out)
+{
+    static bool initialised = false;
+    static int draw_faint_lines = -1;
+    static int autofollow = -1;
+
+    if (!initialised) {
+        char *env;
+
+        initialised = true;
+        draw_faint_lines = getenv_bool("LOOPY_FAINT_LINES", -1);
+
+        if ((env = getenv("LOOPY_AUTOFOLLOW")) != NULL) {
+            if (!strcmp(env, "off"))
+                autofollow = AF_OFF;
+            else if (!strcmp(env, "fixed"))
+                autofollow = AF_FIXED;
+            else if (!strcmp(env, "adaptive"))
+                autofollow = AF_ADAPTIVE;
+        }
+    }
+
+    if (draw_faint_lines != -1)
+        ui_out->draw_faint_lines = draw_faint_lines;
+    if (autofollow != -1)
+        ui_out->autofollow = autofollow;
+}
+
 static game_ui *new_ui(const game_state *state)
 {
-    return NULL;
+    game_ui *ui = snew(game_ui);
+    ui->draw_faint_lines = true;
+    ui->autofollow = AF_OFF;
+    legacy_prefs_override(ui);
+    return ui;
 }
 
 static void free_ui(game_ui *ui)
 {
+    sfree(ui);
 }
 
 static void game_changed_state(game_ui *ui, const game_state *oldstate,
@@ -3024,73 +3078,58 @@
     movesize = 80;
     movebuf = snewn(movesize, char);
     movelen = sprintf(movebuf, "%d%c", i, (int)button_char);
-    {
-        static enum { OFF, FIXED, ADAPTIVE, DUNNO } autofollow = DUNNO;
-        if (autofollow == DUNNO) {
-            const char *env = getenv("LOOPY_AUTOFOLLOW");
-            if (env && !strcmp(env, "off"))
-                autofollow = OFF;
-            else if (env && !strcmp(env, "fixed"))
-                autofollow = FIXED;
-            else if (env && !strcmp(env, "adaptive"))
-                autofollow = ADAPTIVE;
-            else
-                autofollow = OFF;
-        }
 
-        if (autofollow != OFF) {
-            int dotid;
-            for (dotid = 0; dotid < 2; dotid++) {
-                grid_dot *dot = (dotid == 0 ? e->dot1 : e->dot2);
-                grid_edge *e_this = e;
+    if (ui->autofollow != AF_OFF) {
+        int dotid;
+        for (dotid = 0; dotid < 2; dotid++) {
+            grid_dot *dot = (dotid == 0 ? e->dot1 : e->dot2);
+            grid_edge *e_this = e;
 
-                while (1) {
-                    int j, n_found;
-                    grid_edge *e_next = NULL;
+            while (1) {
+                int j, n_found;
+                grid_edge *e_next = NULL;
 
-                    for (j = n_found = 0; j < dot->order; j++) {
-                        grid_edge *e_candidate = dot->edges[j];
-                        int i_candidate = e_candidate - g->edges;
-                        if (e_candidate != e_this &&
-                            (autofollow == FIXED ||
-                             state->lines[i] == LINE_NO ||
-                             state->lines[i_candidate] != LINE_NO)) {
-                            e_next = e_candidate;
-                            n_found++;
-                        }
+                for (j = n_found = 0; j < dot->order; j++) {
+                    grid_edge *e_candidate = dot->edges[j];
+                    int i_candidate = e_candidate - g->edges;
+                    if (e_candidate != e_this &&
+                        (ui->autofollow == AF_FIXED ||
+                         state->lines[i] == LINE_NO ||
+                         state->lines[i_candidate] != LINE_NO)) {
+                        e_next = e_candidate;
+                        n_found++;
                     }
+                }
 
-                    if (n_found != 1 ||
-                        state->lines[e_next - g->edges] != state->lines[i])
-                        break;
+                if (n_found != 1 ||
+                    state->lines[e_next - g->edges] != state->lines[i])
+                    break;
 
-                    if (e_next == e) {
-                        /*
-                         * Special case: we might have come all the
-                         * way round a loop and found our way back to
-                         * the same edge we started from. In that
-                         * situation, we must terminate not only this
-                         * while loop, but the 'for' outside it that
-                         * was tracing in both directions from the
-                         * starting edge, because if we let it trace
-                         * in the second direction then we'll only
-                         * find ourself traversing the same loop in
-                         * the other order and generate an encoded
-                         * move string that mentions the same set of
-                         * edges twice.
-                         */
-                        goto autofollow_done;
-                    }
+                if (e_next == e) {
+                    /*
+                     * Special case: we might have come all the way
+                     * round a loop and found our way back to the same
+                     * edge we started from. In that situation, we
+                     * must terminate not only this while loop, but
+                     * the 'for' outside it that was tracing in both
+                     * directions from the starting edge, because if
+                     * we let it trace in the second direction then
+                     * we'll only find ourself traversing the same
+                     * loop in the other order and generate an encoded
+                     * move string that mentions the same set of edges
+                     * twice.
+                     */
+                    goto autofollow_done;
+                }
 
-                    dot = (e_next->dot1 != dot ? e_next->dot1 : e_next->dot2);
-                    if (movelen > movesize - 40) {
-                        movesize = movesize * 5 / 4 + 128;
-                        movebuf = sresize(movebuf, movesize, char);
-                    }
-                    e_this = e_next;
-                    movelen += sprintf(movebuf+movelen, "%d%c",
-                                       (int)(e_this - g->edges), button_char);
+                dot = (e_next->dot1 != dot ? e_next->dot1 : e_next->dot2);
+                if (movelen > movesize - 40) {
+                    movesize = movesize * 5 / 4 + 128;
+                    movebuf = sresize(movebuf, movesize, char);
                 }
+                e_this = e_next;
+                movelen += sprintf(movebuf+movelen, "%d%c",
+                                   (int)(e_this - g->edges), button_char);
             }
           autofollow_done:;
         }
@@ -3261,7 +3300,7 @@
 };
 #define NPHASES lenof(loopy_line_redraw_phases)
 
-static void game_redraw_line(drawing *dr, game_drawstate *ds,
+static void game_redraw_line(drawing *dr, game_drawstate *ds,const game_ui *ui,
 			     const game_state *state, int i, int phase)
 {
     grid *g = state->game_grid;
@@ -3287,10 +3326,7 @@
     grid_to_screen(ds, g, e->dot2->x, e->dot2->y, &x2, &y2);
 
     if (line_colour == COL_FAINT) {
-	static int draw_faint_lines = -1;
-	if (draw_faint_lines < 0)
-	    draw_faint_lines = getenv_bool("LOOPY_FAINT_LINES", true);
-	if (draw_faint_lines)
+	if (ui->draw_faint_lines)
             draw_thick_line(dr, ds->tilesize/24.0,
                             x1 + 0.5, y1 + 0.5,
                             x2 + 0.5, y2 + 0.5,
@@ -3326,7 +3362,7 @@
 }
 
 static void game_redraw_in_rect(drawing *dr, game_drawstate *ds,
-                                const game_state *state,
+                                const game_ui *ui, const game_state *state,
                                 int x, int y, int w, int h)
 {
     grid *g = state->game_grid;
@@ -3347,7 +3383,7 @@
         for (i = 0; i < g->num_edges; i++) {
             edge_bbox(ds, g, &g->edges[i], &bx, &by, &bw, &bh);
             if (boxes_intersect(x, y, w, h, bx, by, bw, bh))
-                game_redraw_line(dr, ds, state, i, phase);
+                game_redraw_line(dr, ds, ui, state, i, phase);
         }
     }
     for (i = 0; i < g->num_dots; i++) {
@@ -3504,7 +3540,7 @@
         int w = grid_width * ds->tilesize / g->tilesize;
         int h = grid_height * ds->tilesize / g->tilesize;
 
-        game_redraw_in_rect(dr, ds, state,
+        game_redraw_in_rect(dr, ds, ui, state,
                             0, 0, w + 2*border + 1, h + 2*border + 1);
     } else {
 
@@ -3515,7 +3551,7 @@
 	    int x, y, w, h;
 
             face_text_bbox(ds, g, f, &x, &y, &w, &h);
-            game_redraw_in_rect(dr, ds, state, x, y, w, h);
+            game_redraw_in_rect(dr, ds, ui, state, x, y, w, h);
 	}
 
 	for (i = 0; i < nedges; i++) {
@@ -3523,7 +3559,7 @@
             int x, y, w, h;
 
             edge_bbox(ds, g, e, &x, &y, &w, &h);
-            game_redraw_in_rect(dr, ds, state, x, y, w, h);
+            game_redraw_in_rect(dr, ds, ui, state, x, y, w, h);
 	}
     }
 
--- a/map.c
+++ b/map.c
@@ -46,12 +46,6 @@
 #define SIX (FOUR+2)
 
 /*
- * Ghastly run-time configuration option, just for Gareth (again).
- */
-static int flash_type = -1;
-static float flash_length;
-
-/*
  * Difficulty levels. I do some macro ickery here to ensure that my
  * enum and the various forms of my name list always match up.
  */
@@ -2292,8 +2286,37 @@
 
     int cur_x, cur_y, cur_lastmove;
     bool cur_visible, cur_moved;
+
+    /*
+     * User preference to enable alternative versions of the
+     * completion flash. Some users have found the colour-cycling
+     * default version to be a bit eye-twisting.
+     */
+    enum {
+        FLASH_CYCLIC,          /* cycle the four colours of the map */
+        FLASH_EACH_TO_WHITE,   /* turn each colour white in turn */
+        FLASH_ALL_TO_WHITE     /* flash the whole map to white in one go */
+    } flash_type;
 };
 
+static void legacy_prefs_override(struct game_ui *ui_out)
+{
+    static bool initialised = false;
+    static int flash_type = -1;
+
+    if (!initialised) {
+        char *env;
+
+        initialised = true;
+
+        if ((env = getenv("MAP_ALTERNATIVE_FLASH")) != NULL)
+            flash_type = FLASH_EACH_TO_WHITE;
+    }
+
+    if (flash_type != -1)
+        ui_out->flash_type = flash_type;
+}
+
 static game_ui *new_ui(const game_state *state)
 {
     game_ui *ui = snew(game_ui);
@@ -2305,6 +2328,8 @@
     ui->cur_visible = getenv_bool("PUZZLES_SHOW_CURSOR", false);
     ui->cur_moved = false;
     ui->cur_lastmove = 0;
+    ui->flash_type = FLASH_CYCLIC;
+    legacy_prefs_override(ui);
     return ui;
 }
 
@@ -2882,6 +2907,11 @@
     draw_update(dr, COORD(x), COORD(y), TILESIZE, TILESIZE);
 }
 
+static float flash_length(const game_ui *ui)
+{
+    return (ui->flash_type == FLASH_EACH_TO_WHITE ? 0.50F : 0.30F);
+}
+
 static void game_redraw(drawing *dr, game_drawstate *ds,
                         const game_state *oldstate, const game_state *state,
                         int dir, const game_ui *ui,
@@ -2905,10 +2935,10 @@
     }
 
     if (flashtime) {
-	if (flash_type == 1)
-	    flash = (int)(flashtime * FOUR / flash_length);
+	if (ui->flash_type == FLASH_EACH_TO_WHITE)
+	    flash = (int)(flashtime * FOUR / flash_length(ui));
 	else
-	    flash = 1 + (int)(flashtime * THREE / flash_length);
+	    flash = 1 + (int)(flashtime * THREE / flash_length(ui));
     } else
 	flash = -1;
 
@@ -2927,12 +2957,12 @@
 		bv = FOUR;
 
 	    if (flash >= 0) {
-		if (flash_type == 1) {
+		if (ui->flash_type == FLASH_EACH_TO_WHITE) {
 		    if (tv == flash)
 			tv = FOUR;
 		    if (bv == flash)
 			bv = FOUR;
-		} else if (flash_type == 2) {
+		} else if (ui->flash_type == FLASH_ALL_TO_WHITE) {
 		    if (flash % 2)
 			tv = bv = FOUR;
 		} else {
@@ -3062,15 +3092,7 @@
 {
     if (!oldstate->completed && newstate->completed &&
 	!oldstate->cheated && !newstate->cheated) {
-	if (flash_type < 0) {
-	    char *env = getenv("MAP_ALTERNATIVE_FLASH");
-	    if (env)
-		flash_type = atoi(env);
-	    else
-		flash_type = 0;
-	    flash_length = (flash_type == 1 ? 0.50F : 0.30F);
-	}
-	return flash_length;
+	return flash_length(ui);
     } else
 	return 0.0F;
 }
--- a/misc.c
+++ b/misc.c
@@ -202,7 +202,7 @@
     return ret;
 }
 
-bool getenv_bool(const char *name, bool dflt)
+int getenv_bool(const char *name, int dflt)
 {
     char *env = getenv(name);
     if (env == NULL) return dflt;
--- a/pearl.c
+++ b/pearl.c
@@ -1859,8 +1859,41 @@
 
     int curx, cury;        /* grid position of keyboard cursor */
     bool cursor_active;    /* true iff cursor is shown */
+
+    /*
+     * User preference: general visual style of the GUI. GUI_MASYU is
+     * how this puzzle is traditionally presented, with clue dots in
+     * the middle of grid squares, and the solution loop connecting
+     * square-centres. GUI_LOOPY shifts the grid by half a square in
+     * each direction, so that the clue dots are at _vertices_ of the
+     * grid and the solution loop follows the grid edges, which you
+     * could argue is more logical.
+     */
+    enum { GUI_MASYU, GUI_LOOPY } gui_style;
 };
 
+static void legacy_prefs_override(struct game_ui *ui_out)
+{
+    static bool initialised = false;
+    static int gui_style = -1;
+
+    if (!initialised) {
+        initialised = true;
+
+        switch (getenv_bool("PEARL_GUI_LOOPY", -1)) {
+          case 0:
+            gui_style = GUI_MASYU;
+            break;
+          case 1:
+            gui_style = GUI_LOOPY;
+            break;
+        }
+    }
+
+    if (gui_style != -1)
+        ui_out->gui_style = gui_style;
+}
+
 static game_ui *new_ui(const game_state *state)
 {
     game_ui *ui = snew(game_ui);
@@ -1871,6 +1904,9 @@
     ui->cursor_active = getenv_bool("PUZZLES_SHOW_CURSOR", false);
     ui->curx = ui->cury = 0;
 
+    ui->gui_style = GUI_MASYU;
+    legacy_prefs_override(ui);
+
     return ui;
 }
 
@@ -1903,7 +1939,7 @@
 #define HALFSZ (ds->halfsz)
 #define TILE_SIZE (ds->halfsz*2 + 1)
 
-#define BORDER ((get_gui_style() == GUI_LOOPY) ? (TILE_SIZE/8) : (TILE_SIZE/2))
+#define BORDER ((ui->gui_style == GUI_LOOPY) ? (TILE_SIZE/8) : (TILE_SIZE/2))
 
 #define BORDER_WIDTH (max(TILE_SIZE / 32, 1))
 
@@ -1919,21 +1955,6 @@
 #define DS_FLASH (1 << 21)
 #define DS_CURSOR (1 << 22)
 
-enum { GUI_MASYU, GUI_LOOPY };
-
-static int get_gui_style(void)
-{
-    static int gui_style = -1;
-
-    if (gui_style == -1) {
-        if (getenv_bool("PEARL_GUI_LOOPY", false))
-            gui_style = GUI_LOOPY;
-        else
-            gui_style = GUI_MASYU;
-    }
-    return gui_style;
-}
-
 struct game_drawstate {
     int halfsz;
     bool started;
@@ -2422,8 +2443,8 @@
 }
 
 static void draw_lines_specific(drawing *dr, game_drawstate *ds,
-                                int x, int y, unsigned int lflags,
-                                unsigned int shift, int c)
+                                const game_ui *ui, int x, int y,
+                                unsigned int lflags, unsigned int shift, int c)
 {
     int ox = COORD(x), oy = COORD(y);
     int t2 = HALFSZ, t16 = HALFSZ/4;
@@ -2472,7 +2493,7 @@
 	      COL_CURSOR_BACKGROUND : COL_BACKGROUND);
 	      
 
-    if (get_gui_style() == GUI_LOOPY) {
+    if (ui->gui_style == GUI_LOOPY) {
         /* Draw small dot, underneath any lines. */
         draw_circle(dr, cx, cy, t16, COL_GRID, COL_GRID);
     } else {
@@ -2499,7 +2520,7 @@
             draw_line(dr, mx-msz, my-msz, mx+msz, my+msz, COL_BLACK);
             draw_line(dr, mx-msz, my+msz, mx+msz, my-msz, COL_BLACK);
         } else {
-            if (get_gui_style() == GUI_LOOPY) {
+            if (ui->gui_style == GUI_LOOPY) {
                 /* draw grid lines connecting centre of cells */
                 draw_line(dr, cx, cy, cx+xoff, cy+yoff, COL_GRID);
             }
@@ -2509,11 +2530,11 @@
     /* Draw each of the four directions, where laid (or error, or drag, etc.)
      * Order is important here, specifically for the eventual colours of the
      * exposed end caps. */
-    draw_lines_specific(dr, ds, x, y, lflags, 0,
+    draw_lines_specific(dr, ds, ui, x, y, lflags, 0,
                         (lflags & DS_FLASH ? COL_FLASH : COL_BLACK));
-    draw_lines_specific(dr, ds, x, y, lflags, DS_ESHIFT, COL_ERROR);
-    draw_lines_specific(dr, ds, x, y, lflags, DS_DSHIFT, COL_DRAGOFF);
-    draw_lines_specific(dr, ds, x, y, lflags, DS_DSHIFT, COL_DRAGON);
+    draw_lines_specific(dr, ds, ui, x, y, lflags, DS_ESHIFT, COL_ERROR);
+    draw_lines_specific(dr, ds, ui, x, y, lflags, DS_DSHIFT, COL_DRAGOFF);
+    draw_lines_specific(dr, ds, ui, x, y, lflags, DS_DSHIFT, COL_DRAGON);
 
     /* Draw a clue, if present */
     if (clue != NOCLUE) {
@@ -2540,7 +2561,7 @@
     bool force = false;
 
     if (!ds->started) {
-        if (get_gui_style() == GUI_MASYU) {
+        if (ui->gui_style == GUI_MASYU) {
             /*
              * Black rectangle which is the main grid.
              */
@@ -2657,7 +2678,7 @@
     game_drawstate *ds = game_new_drawstate(dr, state);
     game_set_size(dr, ds, NULL, tilesize);
 
-    if (get_gui_style() == GUI_MASYU) {
+    if (ui->gui_style == GUI_MASYU) {
         /* Draw grid outlines (black). */
         for (x = 0; x <= w; x++)
             draw_line(dr, COORD(x), COORD(0), COORD(x), COORD(h), black);
@@ -2689,7 +2710,8 @@
             int cx = COORD(x) + HALFSZ, cy = COORD(y) + HALFSZ;
             int clue = state->shared->clues[y*w+x];
 
-            draw_lines_specific(dr, ds, x, y, state->lines[y*w+x], 0, black);
+            draw_lines_specific(dr, ds, ui, x, y,
+                                state->lines[y*w+x], 0, black);
 
             if (clue != NOCLUE) {
                 int c = (clue == CORNER) ? black : white;
--- a/puzzles.h
+++ b/puzzles.h
@@ -377,7 +377,9 @@
 char *bin2hex(const unsigned char *in, int inlen);
 unsigned char *hex2bin(const char *in, int outlen);
 
-bool getenv_bool(const char *name, bool dflt);
+/* Returns 0 or 1 if the environment variable is set, or dflt if not.
+ * dflt may be a third value if it needs to be. */
+int getenv_bool(const char *name, int dflt);
 
 /* Mixes two colours in specified proportions. */
 void colour_mix(const float src1[3], const float src2[3], float p,
--- a/range.c
+++ b/range.c
@@ -1225,13 +1225,49 @@
 struct game_ui {
     puzzle_size r, c; /* cursor position */
     bool cursor_show;
+
+    /*
+     * User preference option to swap the left and right mouse
+     * buttons.
+     *
+     * The original puzzle submitter thought it would be more useful
+     * to have the left button turn an empty square into a dotted one,
+     * on the grounds that that was what you did most often; I (SGT)
+     * felt instinctively that the left button ought to place black
+     * squares and the right button place dots, on the grounds that
+     * that was consistent with many other puzzles in which the left
+     * button fills in the data used by the solution checker while the
+     * right button places pencil marks for the user's convenience.
+     *
+     * My first beta-player wasn't sure either, so I thought I'd
+     * pre-emptively put in a 'configuration' mechanism just in case.
+     */
+    bool swap_buttons;
 };
 
+static void legacy_prefs_override(struct game_ui *ui_out)
+{
+    static int initialised = false;
+    static int swap_buttons = -1;
+
+    if (!initialised) {
+        initialised = true;
+        swap_buttons = getenv_bool("RANGE_SWAP_BUTTONS", -1);
+    }
+
+    if (swap_buttons != -1)
+        ui_out->swap_buttons = swap_buttons;
+}
+
 static game_ui *new_ui(const game_state *state)
 {
     struct game_ui *ui = snew(game_ui);
     ui->r = ui->c = 0;
     ui->cursor_show = getenv_bool("PUZZLES_SHOW_CURSOR", false);
+
+    ui->swap_buttons = false;
+    legacy_prefs_override(ui);
+
     return ui;
 }
 
@@ -1298,36 +1334,12 @@
     }
 
     if (button == LEFT_BUTTON || button == RIGHT_BUTTON) {
-	/*
-	 * Utterly awful hack, exactly analogous to the one in Slant,
-	 * to configure the left and right mouse buttons the opposite
-	 * way round.
-	 *
-	 * The original puzzle submitter thought it would be more
-	 * useful to have the left button turn an empty square into a
-	 * dotted one, on the grounds that that was what you did most
-	 * often; I (SGT) felt instinctively that the left button
-	 * ought to place black squares and the right button place
-	 * dots, on the grounds that that was consistent with many
-	 * other puzzles in which the left button fills in the data
-	 * used by the solution checker while the right button places
-	 * pencil marks for the user's convenience.
-	 *
-	 * My first beta-player wasn't sure either, so I thought I'd
-	 * pre-emptively put in a 'configuration' mechanism just in
-	 * case.
-	 */
-	{
-	    static int swap_buttons = -1;
-	    if (swap_buttons < 0)
-                swap_buttons = getenv_bool("RANGE_SWAP_BUTTONS", false);
-	    if (swap_buttons) {
-		if (button == LEFT_BUTTON)
-		    button = RIGHT_BUTTON;
-		else
-		    button = LEFT_BUTTON;
-	    }
-	}
+        if (ui->swap_buttons) {
+            if (button == LEFT_BUTTON)
+                button = RIGHT_BUTTON;
+            else
+                button = LEFT_BUTTON;
+        }
     }
 
     switch (button) {
--- a/signpost.c
+++ b/signpost.c
@@ -1387,8 +1387,32 @@
     bool dragging, drag_is_from;
     int sx, sy;         /* grid coords of start cell */
     int dx, dy;         /* pixel coords of drag posn */
+
+    /*
+     * Trivial and foolish configurable option done on purest whim.
+     * With this option enabled, the victory flash is done by rotating
+     * each square in the opposite direction from its immediate
+     * neighbours, so that they behave like a field of interlocking
+     * gears. With it disabled, they all rotate in the same direction.
+     * Choose for yourself which is more brain-twisting :-)
+     */
+    bool gear_mode;
 };
 
+static void legacy_prefs_override(struct game_ui *ui_out)
+{
+    static bool initialised = false;
+    static int gear_mode = -1;
+
+    if (!initialised) {
+        initialised = true;
+        gear_mode = getenv_bool("SIGNPOST_GEARS", -1);
+    }
+
+    if (gear_mode != -1)
+        ui_out->gear_mode = gear_mode;
+}
+
 static game_ui *new_ui(const game_state *state)
 {
     game_ui *ui = snew(game_ui);
@@ -1402,6 +1426,9 @@
     ui->dragging = false;
     ui->sx = ui->sy = ui->dx = ui->dy = 0;
 
+    ui->gear_mode = false;
+    legacy_prefs_override(ui);
+
     return ui;
 }
 
@@ -2143,26 +2170,7 @@
             if (state->nums[i] != ds->nums[i] ||
                 f != ds->f[i] || dirp != ds->dirp[i] ||
                 force || !ds->started) {
-                int sign;
-                {
-                    /*
-                     * Trivial and foolish configurable option done on
-                     * purest whim. With this option enabled, the
-                     * victory flash is done by rotating each square
-                     * in the opposite direction from its immediate
-                     * neighbours, so that they behave like a field of
-                     * interlocking gears. With it disabled, they all
-                     * rotate in the same direction. Choose for
-                     * yourself which is more brain-twisting :-)
-                     */
-                    static int gear_mode = -1;
-                    if (gear_mode < 0)
-                        gear_mode = getenv_bool("SIGNPOST_GEARS", false);
-                    if (gear_mode)
-                        sign = 1 - 2 * ((x ^ y) & 1);
-                    else
-                        sign = 1;
-                }
+                int sign = (ui->gear_mode ? 1 - 2 * ((x ^ y) & 1) : 1);
                 tile_redraw(dr, ds,
                             BORDER + x * TILE_SIZE,
                             BORDER + y * TILE_SIZE,
--- a/slant.c
+++ b/slant.c
@@ -1581,13 +1581,39 @@
 struct game_ui {
     int cur_x, cur_y;
     bool cur_visible;
+
+    /*
+     * User preference option to swap the left and right mouse
+     * buttons. There isn't a completely obvious mapping of left and
+     * right buttons to the two directions of slash, and at least one
+     * player turned out not to have the same intuition as me.
+     */
+    bool swap_buttons;
 };
 
+static void legacy_prefs_override(struct game_ui *ui_out)
+{
+    static bool initialised = false;
+    static int swap_buttons = -1;
+
+    if (!initialised) {
+        initialised = true;
+        swap_buttons = getenv_bool("SLANT_SWAP_BUTTONS", -1);
+    }
+
+    if (swap_buttons != -1)
+        ui_out->swap_buttons = swap_buttons;
+}
+
 static game_ui *new_ui(const game_state *state)
 {
     game_ui *ui = snew(game_ui);
     ui->cur_x = ui->cur_y = 0;
     ui->cur_visible = getenv_bool("PUZZLES_SHOW_CURSOR", false);
+
+    ui->swap_buttons = false;
+    legacy_prefs_override(ui);
+
     return ui;
 }
 
--- a/towers.c
+++ b/towers.c
@@ -1159,8 +1159,37 @@
      * allowed on immutable squares.
      */
     bool hcursor;
+
+    /*
+     * User preference option which can be set to FALSE to disable the
+     * 3D graphical style, and instead just display the puzzle as if
+     * it was a Sudoku variant, i.e. each square just has a digit in
+     * it.
+     *
+     * I was initially a bit uncertain about whether the 3D style
+     * would be the right thing, on the basis that it uses up space in
+     * the cells and makes it hard to use many pencil marks. Actually
+     * nobody seems to have complained, but having put in the option
+     * while I was still being uncertain, it seems silly not to leave
+     * it in just in case.
+     */
+    int three_d;
 };
 
+static void legacy_prefs_override(struct game_ui *ui_out)
+{
+    static bool initialised = false;
+    static int three_d = -1;
+
+    if (!initialised) {
+        initialised = true;
+        three_d = getenv_bool("TOWERS_2D", -1);
+    }
+
+    if (three_d != -1)
+        ui_out->three_d = three_d;
+}
+
 static game_ui *new_ui(const game_state *state)
 {
     game_ui *ui = snew(game_ui);
@@ -1169,6 +1198,9 @@
     ui->hpencil = false;
     ui->hshow = ui->hcursor = getenv_bool("PUZZLES_SHOW_CURSOR", false);
 
+    ui->three_d = true;
+    legacy_prefs_override(ui);
+
     return ui;
 }
 
@@ -1224,7 +1256,6 @@
 
 struct game_drawstate {
     int tilesize;
-    bool three_d;       /* default 3D graphics are user-disableable */
     long *tiles;		       /* (w+2)*(w+2) temp space */
     long *drawn;		       /* (w+2)*(w+2)*4: current drawn data */
     bool *errtmp;
@@ -1356,7 +1387,7 @@
     tx = FROMCOORD(x);
     ty = FROMCOORD(y);
 
-    if (ds->three_d) {
+    if (ui->three_d) {
 	/*
 	 * In 3D mode, just locating the mouse click in the natural
 	 * square grid may not be sufficient to tell which tower the
@@ -1630,7 +1661,6 @@
     int i;
 
     ds->tilesize = 0;
-    ds->three_d = !getenv_bool("TOWERS_2D", false);
     ds->tiles = snewn((w+2)*(w+2), long);
     ds->drawn = snewn((w+2)*(w+2)*4, long);
     for (i = 0; i < (w+2)*(w+2)*4; i++)
@@ -1648,8 +1678,8 @@
     sfree(ds);
 }
 
-static void draw_tile(drawing *dr, game_drawstate *ds, struct clues *clues,
-		      int x, int y, long tile)
+static void draw_tile(drawing *dr, game_drawstate *ds, const game_ui *ui,
+                      struct clues *clues, int x, int y, long tile)
 {
     int w = clues->w /* , a = w*w */;
     int tx, ty, bg;
@@ -1661,7 +1691,7 @@
     bg = (tile & DF_HIGHLIGHT) ? COL_HIGHLIGHT : COL_BACKGROUND;
 
     /* draw tower */
-    if (ds->three_d && (tile & DF_PLAYAREA) && (tile & DF_DIGIT_MASK)) {
+    if (ui->three_d && (tile & DF_PLAYAREA) && (tile & DF_DIGIT_MASK)) {
 	int coords[8];
 	int xoff = X_3D_DISP(tile & DF_DIGIT_MASK, w);
 	int yoff = Y_3D_DISP(tile & DF_DIGIT_MASK, w);
@@ -1762,10 +1792,10 @@
 	     * to put the pencil marks.
 	     */
 	    /* Start with the whole square, minus space for impinging towers */
-	    pl = tx + (ds->three_d ? X_3D_DISP(w,w) : 0);
+	    pl = tx + (ui->three_d ? X_3D_DISP(w,w) : 0);
 	    pr = tx + TILESIZE;
 	    pt = ty;
-	    pb = ty + TILESIZE - (ds->three_d ? Y_3D_DISP(w,w) : 0);
+	    pb = ty + TILESIZE - (ui->three_d ? Y_3D_DISP(w,w) : 0);
 
 	    /*
 	     * We arrange our pencil marks in a grid layout, with
@@ -1901,13 +1931,13 @@
 		ds->drawn[i*4+2] != bl || ds->drawn[i*4+3] != br) {
 		clip(dr, COORD(x-1), COORD(y-1), TILESIZE, TILESIZE);
 
-		draw_tile(dr, ds, state->clues, x-1, y-1, tr);
+		draw_tile(dr, ds, ui, state->clues, x-1, y-1, tr);
 		if (x > 0)
-		    draw_tile(dr, ds, state->clues, x-2, y-1, tl);
+		    draw_tile(dr, ds, ui, state->clues, x-2, y-1, tl);
 		if (y <= w)
-		    draw_tile(dr, ds, state->clues, x-1, y, br);
+		    draw_tile(dr, ds, ui, state->clues, x-1, y, br);
 		if (x > 0 && y <= w)
-		    draw_tile(dr, ds, state->clues, x-2, y, bl);
+		    draw_tile(dr, ds, ui, state->clues, x-2, y, bl);
 
 		unclip(dr);
 		draw_update(dr, COORD(x-1), COORD(y-1), TILESIZE, TILESIZE);