Error-highlight loops in Net.

Loops are detected using the same dsf technique I ended up using in
Slant, and highlighted in red (whether or not the connected component
they belong to is currently powered).

Should make life a little bit easier for someone who's filled in most
of the grid to a nice uniform cyan and finds one piece left over - now
they have some idea where to start looking for their mistake.

We also take care not to generate any loops in the starting position,
on grounds of politeness (don't accuse the user of a mistake before
they've even had a chance to make one).

Loop detection does not contribute to the code that decides whether
the puzzle is complete, because there's no need - if all squares are
connected together, then there can't be any loops anyway, by graph
theory.
This commit is contained in:
Simon Tatham
2014-12-28 12:19:50 +00:00
parent 822243de1b
commit 546fbe6774

183
net.c
View File

@ -41,6 +41,11 @@
#define D 0x08 #define D 0x08
#define LOCKED 0x10 #define LOCKED 0x10
#define ACTIVE 0x20 #define ACTIVE 0x20
#define RLOOP (R << 6)
#define ULOOP (U << 6)
#define LLOOP (L << 6)
#define DLOOP (D << 6)
#define LOOP(dir) ((dir) << 6)
/* Rotations: Anticlockwise, Clockwise, Flip, general rotate */ /* Rotations: Anticlockwise, Clockwise, Flip, general rotate */
#define A(x) ( (((x) & 0x07) << 1) | (((x) & 0x08) >> 3) ) #define A(x) ( (((x) & 0x07) << 1) | (((x) & 0x08) >> 3) )
@ -85,6 +90,7 @@ enum {
COL_ENDPOINT, COL_ENDPOINT,
COL_POWERED, COL_POWERED,
COL_BARRIER, COL_BARRIER,
COL_LOOP,
NCOLOURS NCOLOURS
}; };
@ -1126,6 +1132,10 @@ static void perturb(int w, int h, unsigned char *tiles, int wrapping,
sfree(perimeter); sfree(perimeter);
} }
static int *compute_loops_inner(int w, int h, int wrapping,
const unsigned char *tiles,
const unsigned char *barriers);
static char *new_game_desc(const game_params *params, random_state *rs, static char *new_game_desc(const game_params *params, random_state *rs,
char **aux, int interactive) char **aux, int interactive)
{ {
@ -1424,10 +1434,21 @@ static char *new_game_desc(const game_params *params, random_state *rs,
* connectedness. However, that would take more effort, and * connectedness. However, that would take more effort, and
* it's easier to simply make sure every grid is _obviously_ * it's easier to simply make sure every grid is _obviously_
* not solved.) * not solved.)
*
* We also require that our shuffle produces no loops in the
* initial grid state, because it's a bit rude to light up a 'HEY,
* YOU DID SOMETHING WRONG!' indicator when the user hasn't even
* had a chance to do _anything_ yet. This also is possible just
* by retrying the whole shuffle on failure, because it's clear
* that at least one non-solved shuffle with no loops must exist.
* (Proof: take the _solved_ state of the puzzle, and rotate one
* endpoint.)
*/ */
while (1) { while (1) {
int mismatches; int mismatches, prev_loopsquares, this_loopsquares, i;
int *loops;
shuffle:
for (y = 0; y < h; y++) { for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) { for (x = 0; x < w; x++) {
int orig = index(params, tiles, x, y); int orig = index(params, tiles, x, y);
@ -1436,6 +1457,35 @@ static char *new_game_desc(const game_params *params, random_state *rs,
} }
} }
/*
* Check for loops, and try to fix them by reshuffling just
* the squares involved.
*/
prev_loopsquares = w*h+1;
while (1) {
loops = compute_loops_inner(w, h, params->wrapping, tiles, NULL);
this_loopsquares = 0;
for (i = 0; i < w*h; i++) {
if (loops[i]) {
int orig = tiles[i];
int rot = random_upto(rs, 4);
tiles[i] = ROT(orig, rot);
this_loopsquares++;
}
}
sfree(loops);
if (this_loopsquares > prev_loopsquares) {
/*
* We're increasing rather than reducing the number of
* loops. Give up and go back to the full shuffle.
*/
goto shuffle;
}
if (this_loopsquares == 0)
break;
prev_loopsquares = this_loopsquares;
}
mismatches = 0; mismatches = 0;
/* /*
* I can't even be bothered to check for mismatches across * I can't even be bothered to check for mismatches across
@ -1452,7 +1502,10 @@ static char *new_game_desc(const game_params *params, random_state *rs,
mismatches++; mismatches++;
} }
if (mismatches > 0) if (mismatches == 0)
continue;
/* OK. */
break; break;
} }
@ -1856,6 +1909,94 @@ static unsigned char *compute_active(const game_state *state, int cx, int cy)
return active; return active;
} }
static int *compute_loops_inner(int w, int h, int wrapping,
const unsigned char *tiles,
const unsigned char *barriers)
{
int W = w + 1, H = h + 1;
int *loops, *dsf;
int x, y;
/*
* Construct a dsf covering _vertices_ of the grid, so we have one
* more in each direction than we do squares.
*/
dsf = snew_dsf(W*H);
/*
* For each grid square, unify adjacent vertices of that square
* unless there's a connection separating them. (We only need to
* check the connection in _this_ square, without bothering to
* look for one on the other side of the grid line, because the
* loop will do that anyway when it gets to the other square.)
*
* Barriers break loops, so we disallow any connection which
* terminates in a barrier.
*/
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
int t = tiles[y*w+x];
if (barriers)
t &= ~barriers[y*w+x];
if (!(t & L))
dsf_merge(dsf, y*W+x, (y+1)*W+x);
if (!(t & R))
dsf_merge(dsf, y*W+(x+1), (y+1)*W+(x+1));
if (!(t & U))
dsf_merge(dsf, y*W+x, y*W+(x+1));
if (!(t & D))
dsf_merge(dsf, (y+1)*W+x, (y+1)*W+(x+1));
}
}
/*
* If the game is in wrapping mode, unify each edge vertex with
* its opposite.
*/
if (wrapping) {
for (y = 0; y < H; y++)
dsf_merge(dsf, y*W+0, y*W+w);
for (x = 0; x < W; x++)
dsf_merge(dsf, 0*W+x, h*W+x);
}
loops = snewn(w*h, int);
/*
* Now we've done the loop detection and can read off the output
* flags trivially: any piece of connection whose two sides are
* not in the same dsf class is part of a loop.
*/
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
int t = tiles[y*w+x];
int flags = 0;
if ((t & L) && (dsf_canonify(dsf, y*W+x) !=
dsf_canonify(dsf, (y+1)*W+x)))
flags |= LLOOP;
if ((t & R) && (dsf_canonify(dsf, y*W+(x+1)) !=
dsf_canonify(dsf, (y+1)*W+(x+1))))
flags |= RLOOP;
if ((t & U) && (dsf_canonify(dsf, y*W+x) !=
dsf_canonify(dsf, y*W+(x+1))))
flags |= ULOOP;
if ((t & D) && (dsf_canonify(dsf, (y+1)*W+x) !=
dsf_canonify(dsf, (y+1)*W+(x+1))))
flags |= DLOOP;
loops[y*w+x] = flags;
}
}
sfree(dsf);
return loops;
}
static int *compute_loops(const game_state *state)
{
return compute_loops_inner(state->width, state->height, state->wrapping,
state->tiles, state->barriers);
}
struct game_ui { struct game_ui {
int org_x, org_y; /* origin */ int org_x, org_y; /* origin */
int cx, cy; /* source tile (game coordinates) */ int cx, cy; /* source tile (game coordinates) */
@ -1916,7 +2057,7 @@ struct game_drawstate {
int width, height; int width, height;
int org_x, org_y; int org_x, org_y;
int tilesize; int tilesize;
unsigned char *visible; int *visible;
}; };
/* ---------------------------------------------------------------------- /* ----------------------------------------------------------------------
@ -2290,14 +2431,16 @@ static game_state *execute_move(const game_state *from, const char *move)
static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state) static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state)
{ {
game_drawstate *ds = snew(game_drawstate); game_drawstate *ds = snew(game_drawstate);
int i;
ds->started = FALSE; ds->started = FALSE;
ds->width = state->width; ds->width = state->width;
ds->height = state->height; ds->height = state->height;
ds->org_x = ds->org_y = -1; ds->org_x = ds->org_y = -1;
ds->visible = snewn(state->width * state->height, unsigned char); ds->visible = snewn(state->width * state->height, int);
ds->tilesize = 0; /* undecided yet */ ds->tilesize = 0; /* undecided yet */
memset(ds->visible, 0xFF, state->width * state->height); for (i = 0; i < state->width * state->height; i++)
ds->visible[i] = -1;
return ds; return ds;
} }
@ -2355,6 +2498,13 @@ static float *game_colours(frontend *fe, int *ncolours)
ret[COL_BARRIER * 3 + 1] = 0.0F; ret[COL_BARRIER * 3 + 1] = 0.0F;
ret[COL_BARRIER * 3 + 2] = 0.0F; ret[COL_BARRIER * 3 + 2] = 0.0F;
/*
* Highlighted loops are red as well.
*/
ret[COL_LOOP * 3 + 0] = 1.0F;
ret[COL_LOOP * 3 + 1] = 0.0F;
ret[COL_LOOP * 3 + 2] = 0.0F;
/* /*
* Unpowered endpoints are blue. * Unpowered endpoints are blue.
*/ */
@ -2527,9 +2677,14 @@ static void draw_tile(drawing *dr, const game_state *state, game_drawstate *ds,
ey = (TILE_SIZE - TILE_BORDER - 1.0F) / 2.0F * Y(dir); ey = (TILE_SIZE - TILE_BORDER - 1.0F) / 2.0F * Y(dir);
MATMUL(tx, ty, matrix, ex, ey); MATMUL(tx, ty, matrix, ex, ey);
draw_line(dr, bx+(int)cx, by+(int)cy, draw_line(dr, bx+(int)cx, by+(int)cy,
bx+(int)(cx+tx), by+(int)(cy+ty), col); bx+(int)(cx+tx), by+(int)(cy+ty),
(tile & LOOP(dir)) ? COL_LOOP : col);
} }
} }
/* If we've drawn any loop-highlighted arms, make sure the centre
* point is loop-coloured rather than a later arm overwriting it. */
if (tile & (RLOOP | ULOOP | LLOOP | DLOOP))
draw_rect(dr, bx+(int)cx, by+(int)cy, 1, 1, COL_LOOP);
/* /*
* Draw the box in the middle. We do this in blue if the tile * Draw the box in the middle. We do this in blue if the tile
@ -2598,7 +2753,9 @@ static void draw_tile(drawing *dr, const game_state *state, game_drawstate *ds,
*/ */
draw_rect_coords(dr, px-vx, py-vy, px+lx+vx, py+ly+vy, COL_WIRE); draw_rect_coords(dr, px-vx, py-vy, px+lx+vx, py+ly+vy, COL_WIRE);
draw_rect_coords(dr, px, py, px+lx, py+ly, draw_rect_coords(dr, px, py, px+lx, py+ly,
(tile & ACTIVE) ? COL_POWERED : COL_WIRE); ((tile & LOOP(dir)) ? COL_LOOP :
(tile & ACTIVE) ? COL_POWERED :
COL_WIRE));
} else { } else {
/* /*
* The other tile extends into our border, but isn't * The other tile extends into our border, but isn't
@ -2672,6 +2829,7 @@ static void game_redraw(drawing *dr, game_drawstate *ds,
{ {
int x, y, tx, ty, frame, last_rotate_dir, moved_origin = FALSE; int x, y, tx, ty, frame, last_rotate_dir, moved_origin = FALSE;
unsigned char *active; unsigned char *active;
int *loops;
float angle = 0.0; float angle = 0.0;
/* /*
@ -2765,11 +2923,13 @@ static void game_redraw(drawing *dr, game_drawstate *ds,
* Draw any tile which differs from the way it was last drawn. * Draw any tile which differs from the way it was last drawn.
*/ */
active = compute_active(state, ui->cx, ui->cy); active = compute_active(state, ui->cx, ui->cy);
loops = compute_loops(state);
for (x = 0; x < ds->width; x++) for (x = 0; x < ds->width; x++)
for (y = 0; y < ds->height; y++) { for (y = 0; y < ds->height; y++) {
unsigned char c = tile(state, GX(x), GY(y)) | int c = tile(state, GX(x), GY(y)) |
index(state, active, GX(x), GY(y)); index(state, active, GX(x), GY(y)) |
index(state, loops, GX(x), GY(y));
int is_src = GX(x) == ui->cx && GY(y) == ui->cy; int is_src = GX(x) == ui->cx && GY(y) == ui->cy;
int is_anim = GX(x) == tx && GY(y) == ty; int is_anim = GX(x) == tx && GY(y) == ty;
int is_cursor = ui->cur_visible && int is_cursor = ui->cur_visible &&
@ -2796,12 +2956,12 @@ static void game_redraw(drawing *dr, game_drawstate *ds,
if (moved_origin || if (moved_origin ||
index(state, ds->visible, x, y) != c || index(state, ds->visible, x, y) != c ||
index(state, ds->visible, x, y) == 0xFF || index(state, ds->visible, x, y) == -1 ||
is_src || is_anim || is_cursor) { is_src || is_anim || is_cursor) {
draw_tile(dr, state, ds, x, y, c, draw_tile(dr, state, ds, x, y, c,
is_src, (is_anim ? angle : 0.0F), is_cursor); is_src, (is_anim ? angle : 0.0F), is_cursor);
if (is_src || is_anim || is_cursor) if (is_src || is_anim || is_cursor)
index(state, ds->visible, x, y) = 0xFF; index(state, ds->visible, x, y) = -1;
else else
index(state, ds->visible, x, y) = c; index(state, ds->visible, x, y) = c;
} }
@ -2830,6 +2990,7 @@ static void game_redraw(drawing *dr, game_drawstate *ds,
} }
sfree(active); sfree(active);
sfree(loops);
} }
static float game_anim_length(const game_state *oldstate, static float game_anim_length(const game_state *oldstate,