slant: add preference: fading grounded components

Commit message added by SGT:

The general idea is that the puzzle constraint "you must not form a
loop" can also be phrased as "every grid vertex must have a path to
the boundary of the grid", on the basis that in a grid with every
square filled, any connected component of vertices _not_ joined to the
boundary by a path must instead be surrounded by a loop blocking every
way it could get there. So by changing the colour of each
"grounded" (connected to the boundary) grid edge, you draw the
player's attention to the components that don't yet have a path to the
edge, so they can consider the possible paths.

This is the kind of hint that users can very easily find to be
patronising and intrusive, not to mention a spoiler if they haven't
even made the leap from "no loops" to "everything must be grounded".
So it's a preference, and off by default.

This patch is somewhat rewritten from the submitted version, to
conform to local style and also compile in older C versions.
This commit is contained in:
Hauke Rehr
2025-02-09 20:47:07 +01:00
committed by Simon Tatham
parent dc2407ed0c
commit b99f10727a

70
slant.c
View File

@ -45,11 +45,13 @@ enum {
COL_ERROR, COL_ERROR,
COL_CURSOR, COL_CURSOR,
COL_FILLEDSQUARE, COL_FILLEDSQUARE,
COL_GROUNDED,
NCOLOURS NCOLOURS
}; };
enum { enum {
PREF_MOUSE_BUTTON_ORDER, PREF_MOUSE_BUTTON_ORDER,
PREF_FADE_GROUNDED,
N_PREF_ITEMS N_PREF_ITEMS
}; };
@ -94,6 +96,7 @@ typedef struct game_clues {
#define ERR_VERTEX 1 #define ERR_VERTEX 1
#define ERR_SQUARE 2 #define ERR_SQUARE 2
#define BORDER_EDGE 4 /* kind of an abuse: not an error */
struct game_state { struct game_state {
struct game_params p; struct game_params p;
@ -1399,6 +1402,48 @@ static bool check_completion(game_state *state)
memset(state->errors, 0, W*H); memset(state->errors, 0, W*H);
/*
* Detect and grounded-highlight edge-connected components in the grid.
*/
{
DSF *connected = dsf_new(W*H);
unsigned root_NW;
int slash;
int x, y;
for (x = 0; x <= w; x++) {
dsf_merge(connected, x, 0);
dsf_merge(connected, h*W+x, 0);
}
for (y = 0; y <= h; y++) {
dsf_merge(connected, y*W, 0);
dsf_merge(connected, y*W+w, 0);
}
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
switch (state->soln[y*w+x]) {
case -1:
dsf_merge(connected, y*W+x, (y+1)*W+(x+1));
break;
case +1:
dsf_merge(connected, y*W+(x+1), (y+1)*W+x);
break;
default:
continue;
}
}
}
root_NW = dsf_canonify(connected, 0);
for (y = 0; y < h; y++)
for (x = 0; x < w; x++)
if ((slash = state->soln[y*w+x]) && dsf_canonify(
connected, y*W + x + (slash == 1 ? 1 : 0)) == root_NW)
state->errors[y*w+x] |= BORDER_EDGE;
dsf_free(connected);
}
/* /*
* Detect and error-highlight loops in the grid. * Detect and error-highlight loops in the grid.
*/ */
@ -1594,6 +1639,7 @@ struct game_ui {
* player turned out not to have the same intuition as me. * player turned out not to have the same intuition as me.
*/ */
bool swap_buttons; bool swap_buttons;
bool fade_grounded;
}; };
static void legacy_prefs_override(struct game_ui *ui_out) static void legacy_prefs_override(struct game_ui *ui_out)
@ -1617,6 +1663,7 @@ static game_ui *new_ui(const game_state *state)
ui->cur_visible = getenv_bool("PUZZLES_SHOW_CURSOR", false); ui->cur_visible = getenv_bool("PUZZLES_SHOW_CURSOR", false);
ui->swap_buttons = false; ui->swap_buttons = false;
ui->fade_grounded = false;
legacy_prefs_override(ui); legacy_prefs_override(ui);
return ui; return ui;
@ -1636,6 +1683,11 @@ static config_item *get_prefs(game_ui *ui)
ret[PREF_MOUSE_BUTTON_ORDER].u.choices.choicekws = ":\\:/"; ret[PREF_MOUSE_BUTTON_ORDER].u.choices.choicekws = ":\\:/";
ret[PREF_MOUSE_BUTTON_ORDER].u.choices.selected = ui->swap_buttons; ret[PREF_MOUSE_BUTTON_ORDER].u.choices.selected = ui->swap_buttons;
ret[PREF_FADE_GROUNDED].name = "Fade grounded components";
ret[PREF_FADE_GROUNDED].kw = "fade-grounded";
ret[PREF_FADE_GROUNDED].type = C_BOOLEAN;
ret[PREF_FADE_GROUNDED].u.boolean.bval = ui->fade_grounded;
ret[N_PREF_ITEMS].name = NULL; ret[N_PREF_ITEMS].name = NULL;
ret[N_PREF_ITEMS].type = C_END; ret[N_PREF_ITEMS].type = C_END;
@ -1645,6 +1697,7 @@ static config_item *get_prefs(game_ui *ui)
static void set_prefs(game_ui *ui, const config_item *cfg) static void set_prefs(game_ui *ui, const config_item *cfg)
{ {
ui->swap_buttons = cfg[PREF_MOUSE_BUTTON_ORDER].u.choices.selected; ui->swap_buttons = cfg[PREF_MOUSE_BUTTON_ORDER].u.choices.selected;
ui->fade_grounded = cfg[PREF_FADE_GROUNDED].u.boolean.bval;
} }
static void free_ui(game_ui *ui) static void free_ui(game_ui *ui)
@ -1705,6 +1758,7 @@ static const char *current_key_label(const game_ui *ui,
#define ERR_BL 0x00010000L #define ERR_BL 0x00010000L
#define ERR_BR 0x00020000L #define ERR_BR 0x00020000L
#define CURSOR 0x00040000L #define CURSOR 0x00040000L
#define GROUNDED 0x00080000L
struct game_drawstate { struct game_drawstate {
int tilesize; int tilesize;
@ -1873,6 +1927,10 @@ static float *game_colours(frontend *fe, int *ncolours)
ret[COL_ERROR * 3 + 1] = 0.0F; ret[COL_ERROR * 3 + 1] = 0.0F;
ret[COL_ERROR * 3 + 2] = 0.0F; ret[COL_ERROR * 3 + 2] = 0.0F;
ret[COL_GROUNDED * 3 + 0] = ret[COL_BACKGROUND * 3 + 0] * 0.8F;
ret[COL_GROUNDED * 3 + 1] = ret[COL_BACKGROUND * 3 + 1] * 0.8F;
ret[COL_GROUNDED * 3 + 2] = ret[COL_BACKGROUND * 3 + 2] * 0.8F;
*ncolours = NCOLOURS; *ncolours = NCOLOURS;
return ret; return ret;
} }
@ -1957,14 +2015,16 @@ static void draw_tile(drawing *dr, game_drawstate *ds, game_clues *clues,
* Draw the slash. * Draw the slash.
*/ */
if (v & BACKSLASH) { if (v & BACKSLASH) {
int scol = (v & ERRSLASH) ? COL_ERROR : bscol; int scol = ((v & ERRSLASH) ? COL_ERROR :
(v & GROUNDED) ? COL_GROUNDED : bscol);
draw_line(dr, COORD(x), COORD(y), COORD(x+1), COORD(y+1), scol); draw_line(dr, COORD(x), COORD(y), COORD(x+1), COORD(y+1), scol);
draw_line(dr, COORD(x)+1, COORD(y), COORD(x+1), COORD(y+1)-1, draw_line(dr, COORD(x)+1, COORD(y), COORD(x+1), COORD(y+1)-1,
scol); scol);
draw_line(dr, COORD(x), COORD(y)+1, COORD(x+1)-1, COORD(y+1), draw_line(dr, COORD(x), COORD(y)+1, COORD(x+1)-1, COORD(y+1),
scol); scol);
} else if (v & FORWSLASH) { } else if (v & FORWSLASH) {
int scol = (v & ERRSLASH) ? COL_ERROR : fscol; int scol = ((v & ERRSLASH) ? COL_ERROR :
(v & GROUNDED) ? COL_GROUNDED : fscol);
draw_line(dr, COORD(x+1), COORD(y), COORD(x), COORD(y+1), scol); draw_line(dr, COORD(x+1), COORD(y), COORD(x), COORD(y+1), scol);
draw_line(dr, COORD(x+1)-1, COORD(y), COORD(x), COORD(y+1)-1, draw_line(dr, COORD(x+1)-1, COORD(y), COORD(x), COORD(y+1)-1,
scol); scol);
@ -2076,6 +2136,12 @@ static void game_redraw(drawing *dr, game_drawstate *ds,
ds->todraw[(y+1)*(w+2)+x] |= ERR_TR; ds->todraw[(y+1)*(w+2)+x] |= ERR_TR;
ds->todraw[(y+1)*(w+2)+(x+1)] |= ERR_TL; ds->todraw[(y+1)*(w+2)+(x+1)] |= ERR_TL;
} }
if (ui->fade_grounded)
for (y = 0; y < h; y++)
for (x = 0; x < w; x++)
if (state->errors[y*w+x] & BORDER_EDGE)
// dunno why but it works this way
ds->todraw[(y+1)*(W+1)+(x+1)] |= GROUNDED;
/* /*
* Now go through and draw the grid squares. * Now go through and draw the grid squares.