char *lines;
unsigned char *line_errors;
+ int exactly_one_loop;
int solved;
int cheated;
char *clue_satisfied;
};
-static char *validate_desc(game_params *params, char *desc);
+static char *validate_desc(const game_params *params, const char *desc);
static int dot_order(const game_state* state, int i, char line_type);
static int face_order(const game_state* state, int i, char line_type);
static solver_state *solve_game_rec(const solver_state *sstate);
/* Generates a (dynamically allocated) new grid, according to the
* type and size requested in params. Does nothing if the grid is already
* generated. */
-static grid *loopy_generate_grid(game_params *params, char *grid_desc)
+static grid *loopy_generate_grid(const game_params *params,
+ const char *grid_desc)
{
return grid_new(grid_types[params->type], params->w, params->h, grid_desc);
}
* General struct manipulation and other straightforward code
*/
-static game_state *dup_game(game_state *state)
+static game_state *dup_game(const game_state *state)
{
game_state *ret = snew(game_state);
ret->line_errors = snewn(state->game_grid->num_edges, unsigned char);
memcpy(ret->line_errors, state->line_errors, state->game_grid->num_edges);
+ ret->exactly_one_loop = state->exactly_one_loop;
ret->grid_type = state->grid_type;
return ret;
}
}
-static solver_state *new_solver_state(game_state *state, int diff) {
+static solver_state *new_solver_state(const game_state *state, int diff) {
int i;
int num_dots = state->game_grid->num_dots;
int num_faces = state->game_grid->num_faces;
return ret;
}
-static game_params *dup_params(game_params *params)
+static game_params *dup_params(const game_params *params)
{
game_params *ret = snew(game_params);
}
}
-static char *encode_params(game_params *params, int full)
+static char *encode_params(const game_params *params, int full)
{
char str[80];
sprintf(str, "%dx%dt%d", params->w, params->h, params->type);
return dupstr(str);
}
-static config_item *game_configure(game_params *params)
+static config_item *game_configure(const game_params *params)
{
config_item *ret;
char buf[80];
return ret;
}
-static game_params *custom_params(config_item *cfg)
+static game_params *custom_params(const config_item *cfg)
{
game_params *ret = snew(game_params);
return ret;
}
-static char *validate_params(game_params *params, int full)
+static char *validate_params(const game_params *params, int full)
{
if (params->type < 0 || params->type >= NUM_GRID_TYPES)
return "Illegal grid type";
/* Splits up a (optional) grid_desc from the game desc. Returns the
* grid_desc (which needs freeing) and updates the desc pointer to
* start of real desc, or returns NULL if no desc. */
-static char *extract_grid_desc(char **desc)
+static char *extract_grid_desc(const char **desc)
{
char *sep = strchr(*desc, GRID_DESC_SEP), *gd;
int gd_len;
/* We require that the params pass the test in validate_params and that the
* description fills the entire game area */
-static char *validate_desc(game_params *params, char *desc)
+static char *validate_desc(const game_params *params, const char *desc)
{
int count = 0;
grid *g;
return ret;
}
-static game_ui *new_ui(game_state *state)
+static game_ui *new_ui(const game_state *state)
{
return NULL;
}
{
}
-static char *encode_ui(game_ui *ui)
+static char *encode_ui(const game_ui *ui)
{
return NULL;
}
-static void decode_ui(game_ui *ui, char *encoding)
+static void decode_ui(game_ui *ui, const char *encoding)
{
}
-static void game_changed_state(game_ui *ui, game_state *oldstate,
- game_state *newstate)
+static void game_changed_state(game_ui *ui, const game_state *oldstate,
+ const game_state *newstate)
{
}
-static void game_compute_size(game_params *params, int tilesize,
+static void game_compute_size(const game_params *params, int tilesize,
int *x, int *y)
{
int grid_width, grid_height, rendered_width, rendered_height;
}
static void game_set_size(drawing *dr, game_drawstate *ds,
- game_params *params, int tilesize)
+ const game_params *params, int tilesize)
{
ds->tilesize = tilesize;
}
static float *game_colours(frontend *fe, int *ncolours)
{
- float *ret = snewn(4 * NCOLOURS, float);
+ float *ret = snewn(3 * NCOLOURS, float);
frontend_default_colour(fe, &ret[COL_BACKGROUND * 3]);
return ret;
}
-static game_drawstate *game_new_drawstate(drawing *dr, game_state *state)
+static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state)
{
struct game_drawstate *ds = snew(struct game_drawstate);
int num_faces = state->game_grid->num_faces;
sfree(ds);
}
-static int game_timing_state(game_state *state, game_ui *ui)
+static int game_timing_state(const game_state *state, game_ui *ui)
{
return TRUE;
}
-static float game_anim_length(game_state *oldstate, game_state *newstate,
- int dir, game_ui *ui)
+static float game_anim_length(const game_state *oldstate,
+ const game_state *newstate, int dir, game_ui *ui)
{
return 0.0F;
}
-static int game_can_format_as_text_now(game_params *params)
+static int game_can_format_as_text_now(const game_params *params)
{
if (params->type != 0)
return FALSE;
return TRUE;
}
-static char *game_text_format(game_state *state)
+static char *game_text_format(const game_state *state)
{
int w, h, W, H;
int x, y, i;
}
-static char *new_game_desc(game_params *params, random_state *rs,
+static char *new_game_desc(const game_params *params, random_state *rs,
char **aux, int interactive)
{
/* solution and description both use run-length encoding in obvious ways */
state->clues = snewn(g->num_faces, signed char);
state->lines = snewn(g->num_edges, char);
state->line_errors = snewn(g->num_edges, unsigned char);
+ state->exactly_one_loop = FALSE;
state->grid_type = params->type;
return retval;
}
-static game_state *new_game(midend *me, game_params *params, char *desc)
+static game_state *new_game(midend *me, const game_params *params,
+ const char *desc)
{
int i;
game_state *state = snew(game_state);
state->clues = snewn(num_faces, signed char);
state->lines = snewn(num_edges, char);
state->line_errors = snewn(num_edges, unsigned char);
+ state->exactly_one_loop = FALSE;
state->solved = state->cheated = FALSE;
static int check_completion(game_state *state)
{
grid *g = state->game_grid;
- int *dsf;
- int num_faces = g->num_faces;
- int i;
- int infinite_area, finite_area;
- int loops_found = 0;
- int found_edge_not_in_loop = FALSE;
+ int i, ret;
+ int *dsf, *component_state;
+ int nsilly, nloop, npath, largest_comp, largest_size;
+ enum { COMP_NONE, COMP_LOOP, COMP_PATH, COMP_SILLY, COMP_EMPTY };
memset(state->line_errors, 0, g->num_edges);
- /* LL implementation of SGT's idea:
- * A loop will partition the grid into an inside and an outside.
- * If there is more than one loop, the grid will be partitioned into
- * even more distinct regions. We can therefore track equivalence of
- * faces, by saying that two faces are equivalent when there is a non-YES
- * edge between them.
- * We could keep track of the number of connected components, by counting
- * the number of dsf-merges that aren't no-ops.
- * But we're only interested in 3 separate cases:
- * no loops, one loop, more than one loop.
+ /*
+ * Find loops in the grid, and determine whether the puzzle is
+ * solved.
+ *
+ * Loopy is a bit more complicated than most puzzles that care
+ * about loop detection. In most of them, loops are simply
+ * _forbidden_; so the obviously right way to do
+ * error-highlighting during play is to light up a graph edge red
+ * iff it is part of a loop, which is exactly what the centralised
+ * findloop.c makes easy.
+ *
+ * But Loopy is unusual in that you're _supposed_ to be making a
+ * loop - and yet _some_ loops are not the right loop. So we need
+ * to be more discriminating, by identifying loops one by one and
+ * then thinking about which ones to highlight, and so findloop.c
+ * isn't quite the right tool for the job in this case.
+ *
+ * Worse still, consider situations in which the grid contains a
+ * loop and also some non-loop edges: there are some cases like
+ * this in which the user's intuitive expectation would be to
+ * highlight the loop (if you're only about half way through the
+ * puzzle and have accidentally made a little loop in some corner
+ * of the grid), and others in which they'd be more likely to
+ * expect you to highlight the non-loop edges (if you've just
+ * closed off a whole loop that you thought was the entire
+ * solution, but forgot some disconnected edges in a corner
+ * somewhere). So while it's easy enough to check whether the
+ * solution is _right_, highlighting the wrong parts is a tricky
+ * problem for this puzzle!
+ *
+ * I'd quite like, in some situations, to identify the largest
+ * loop among the player's YES edges, and then light up everything
+ * other than that. But finding the longest cycle in a graph is an
+ * NP-complete problem (because, in particular, it must return a
+ * Hamilton cycle if one exists).
+ *
+ * However, I think we can make the problem tractable by
+ * exercising the Puzzles principle that it isn't absolutely
+ * necessary to highlight _all_ errors: the key point is that by
+ * the time the user has filled in the whole grid, they should
+ * either have seen a completion flash, or have _some_ error
+ * highlight showing them why the solution isn't right. So in
+ * principle it would be *just about* good enough to highlight
+ * just one error in the whole grid, if there was really no better
+ * way. But we'd like to highlight as many errors as possible.
+ *
+ * In this case, I think the simple approach is to make use of the
+ * fact that no vertex may have degree > 2, and that's really
+ * simple to detect. So the plan goes like this:
+ *
+ * - Form the dsf of connected components of the graph vertices.
*
- * No loops: all faces are equivalent to the infinite face.
- * One loop: only two equivalence classes - finite and infinite.
- * >= 2 loops: there are 2 distinct finite regions.
+ * - Highlight an error at any vertex with degree > 2. (It so
+ * happens that we do this by lighting up all the edges
+ * incident to that vertex, but that's an output detail.)
*
- * So we simply make two passes through all the edges.
- * In the first pass, we dsf-merge the two faces bordering each non-YES
- * edge.
- * In the second pass, we look for YES-edges bordering:
- * a) two non-equivalent faces.
- * b) two non-equivalent faces, and one of them is part of a different
- * finite area from the first finite area we've seen.
+ * - Any component that contains such a vertex is now excluded
+ * from further consideration, because it already has a
+ * highlight.
*
- * An occurrence of a) means there is at least one loop.
- * An occurrence of b) means there is more than one loop.
- * Edges satisfying a) are marked as errors.
+ * - The remaining components have no vertex with degree > 2, and
+ * hence they all consist of either a simple loop, or a simple
+ * path with two endpoints.
*
- * While we're at it, we set a flag if we find a YES edge that is not
- * part of a loop.
- * This information will help decide, if there's a single loop, whether it
- * is a candidate for being a solution (that is, all YES edges are part of
- * this loop).
+ * - If the sensible components are all paths, or if there's
+ * exactly one of them and it is a loop, then highlight no
+ * further edge errors. (The former case is normal during play,
+ * and the latter is a potentially solved puzzle.)
*
- * If there is a candidate loop, we then go through all clues and check
- * they are all satisfied. If so, we have found a solution and we can
- * unmark all line_errors.
+ * - Otherwise - if there is more than one sensible component
+ * _and_ at least one of them is a loop - find the largest of
+ * the sensible components, leave that one unhighlighted, and
+ * light the rest up in red.
*/
-
- /* Infinite face is at the end - its index is num_faces.
- * This macro is just to make this obvious! */
- #define INF_FACE num_faces
- dsf = snewn(num_faces + 1, int);
- dsf_init(dsf, num_faces + 1);
-
- /* First pass */
- for (i = 0; i < g->num_edges; i++) {
- grid_edge *e = g->edges + i;
- int f1 = e->face1 ? e->face1 - g->faces : INF_FACE;
- int f2 = e->face2 ? e->face2 - g->faces : INF_FACE;
- if (state->lines[i] != LINE_YES)
- dsf_merge(dsf, f1, f2);
- }
-
- /* Second pass */
- infinite_area = dsf_canonify(dsf, INF_FACE);
- finite_area = -1;
- for (i = 0; i < g->num_edges; i++) {
- grid_edge *e = g->edges + i;
- int f1 = e->face1 ? e->face1 - g->faces : INF_FACE;
- int can1 = dsf_canonify(dsf, f1);
- int f2 = e->face2 ? e->face2 - g->faces : INF_FACE;
- int can2 = dsf_canonify(dsf, f2);
- if (state->lines[i] != LINE_YES) continue;
-
- if (can1 == can2) {
- /* Faces are equivalent, so this edge not part of a loop */
- found_edge_not_in_loop = TRUE;
- continue;
- }
- state->line_errors[i] = TRUE;
- if (loops_found == 0) loops_found = 1;
- /* Don't bother with further checks if we've already found 2 loops */
- if (loops_found == 2) continue;
+ dsf = snew_dsf(g->num_dots);
- if (finite_area == -1) {
- /* Found our first finite area */
- if (can1 != infinite_area)
- finite_area = can1;
- else
- finite_area = can2;
- }
-
- /* Have we found a second area? */
- if (finite_area != -1) {
- if (can1 != infinite_area && can1 != finite_area) {
- loops_found = 2;
- continue;
- }
- if (can2 != infinite_area && can2 != finite_area) {
- loops_found = 2;
- }
+ /* Build the dsf. */
+ for (i = 0; i < g->num_edges; i++) {
+ if (state->lines[i] == LINE_YES) {
+ grid_edge *e = g->edges + i;
+ int d1 = e->dot1 - g->dots, d2 = e->dot2 - g->dots;
+ dsf_merge(dsf, d1, d2);
}
}
-/*
- printf("loops_found = %d\n", loops_found);
- printf("found_edge_not_in_loop = %s\n",
- found_edge_not_in_loop ? "TRUE" : "FALSE");
-*/
-
- sfree(dsf); /* No longer need the dsf */
-
- /* Have we found a candidate loop? */
- if (loops_found == 1 && !found_edge_not_in_loop) {
- /* Yes, so check all clues are satisfied */
- int found_clue_violation = FALSE;
- for (i = 0; i < num_faces; i++) {
- int c = state->clues[i];
- if (c >= 0) {
- if (face_order(state, i, LINE_YES) != c) {
- found_clue_violation = TRUE;
- break;
- }
- }
- }
-
- if (!found_clue_violation) {
- /* The loop is good */
- memset(state->line_errors, 0, g->num_edges);
- return TRUE; /* No need to bother checking for dot violations */
- }
+ /* Initialise a state variable for each connected component. */
+ component_state = snewn(g->num_dots, int);
+ for (i = 0; i < g->num_dots; i++) {
+ if (dsf_canonify(dsf, i) == i)
+ component_state[i] = COMP_LOOP;
+ else
+ component_state[i] = COMP_NONE;
}
- /* Check for dot violations */
+ /* Check for dots with degree > 3. Here we also spot dots of
+ * degree 1 in which the user has marked all the non-edges as
+ * LINE_NO, because those are also clear vertex-level errors, so
+ * we give them the same treatment of excluding their connected
+ * component from the subsequent loop analysis. */
for (i = 0; i < g->num_dots; i++) {
+ int comp = dsf_canonify(dsf, i);
int yes = dot_order(state, i, LINE_YES);
int unknown = dot_order(state, i, LINE_UNKNOWN);
if ((yes == 1 && unknown == 0) || (yes >= 3)) {
if (state->lines[e] == LINE_YES)
state->line_errors[e] = TRUE;
}
+ /* And mark this component as not worthy of further
+ * consideration. */
+ component_state[comp] = COMP_SILLY;
+
+ } else if (yes == 0) {
+ /* A completely isolated dot must also be excluded it from
+ * the subsequent loop highlighting pass, but we tag it
+ * with a different enum value to avoid it counting
+ * towards the components that inhibit returning a win
+ * status. */
+ component_state[comp] = COMP_EMPTY;
+ } else if (yes == 1) {
+ /* A dot with degree 1 that didn't fall into the 'clearly
+ * erroneous' case above indicates that this connected
+ * component will be a path rather than a loop - unless
+ * something worse elsewhere in the component has
+ * classified it as silly. */
+ if (component_state[comp] != COMP_SILLY)
+ component_state[comp] = COMP_PATH;
}
}
- return FALSE;
+
+ /* Count up the components. Also, find the largest sensible
+ * component. (Tie-breaking condition is derived from the order of
+ * vertices in the grid data structure, which is fairly arbitrary
+ * but at least stays stable throughout the game.) */
+ nsilly = nloop = npath = 0;
+ largest_comp = largest_size = -1;
+ for (i = 0; i < g->num_dots; i++) {
+ if (component_state[i] == COMP_SILLY) {
+ nsilly++;
+ } else if (component_state[i] == COMP_PATH ||
+ component_state[i] == COMP_LOOP) {
+ int this_size;
+
+ if (component_state[i] == COMP_PATH)
+ npath++;
+ else if (component_state[i] == COMP_LOOP)
+ nloop++;
+
+ if ((this_size = dsf_size(dsf, i)) > largest_size) {
+ largest_comp = i;
+ largest_size = this_size;
+ }
+ }
+ }
+
+ if (nloop > 0 && nloop + npath > 1) {
+ /*
+ * If there are at least two sensible components including at
+ * least one loop, highlight all edges in every sensible
+ * component that is not the largest one.
+ */
+ for (i = 0; i < g->num_edges; i++) {
+ if (state->lines[i] == LINE_YES) {
+ grid_edge *e = g->edges + i;
+ int d1 = e->dot1 - g->dots; /* either endpoint is good enough */
+ int comp = dsf_canonify(dsf, d1);
+ if (component_state[comp] != COMP_SILLY &&
+ comp != largest_comp)
+ state->line_errors[i] = TRUE;
+ }
+ }
+ }
+
+ if (nloop == 1 && npath == 0 && nsilly == 0) {
+ /*
+ * If there is exactly one component and it is a loop, then
+ * the puzzle is potentially complete, so check the clues.
+ */
+ ret = TRUE;
+
+ for (i = 0; i < g->num_faces; i++) {
+ int c = state->clues[i];
+ if (c >= 0 && face_order(state, i, LINE_YES) != c) {
+ ret = FALSE;
+ break;
+ }
+ }
+
+ /*
+ * Also, whether or not the puzzle is actually complete, set
+ * the flag that says this game_state has exactly one loop and
+ * nothing else, which will be used to vary the semantics of
+ * clue highlighting at display time.
+ */
+ state->exactly_one_loop = TRUE;
+ } else {
+ ret = FALSE;
+ state->exactly_one_loop = FALSE;
+ }
+
+ sfree(component_state);
+ sfree(dsf);
+
+ return ret;
}
/* ----------------------------------------------------------------------
return sstate;
}
-static char *solve_game(game_state *state, game_state *currstate,
- char *aux, char **error)
+static char *solve_game(const game_state *state, const game_state *currstate,
+ const char *aux, char **error)
{
char *soln = NULL;
solver_state *sstate, *new_sstate;
* Drawing and mouse-handling
*/
-static char *interpret_move(game_state *state, game_ui *ui, const game_drawstate *ds,
+static char *interpret_move(const game_state *state, game_ui *ui,
+ const game_drawstate *ds,
int x, int y, int button)
{
grid *g = state->game_grid;
return ret;
}
-static game_state *execute_move(game_state *state, char *move)
+static game_state *execute_move(const game_state *state, const char *move)
{
int i;
game_state *newstate = dup_game(state);
}
static void game_redraw_clue(drawing *dr, game_drawstate *ds,
- game_state *state, int i)
+ const game_state *state, int i)
{
grid *g = state->game_grid;
grid_face *f = g->faces + i;
int x, y;
- char c[3];
+ char c[20];
- if (state->clues[i] < 10) {
- c[0] = CLUE2CHAR(state->clues[i]);
- c[1] = '\0';
- } else {
- sprintf(c, "%d", state->clues[i]);
- }
+ sprintf(c, "%d", state->clues[i]);
face_text_pos(ds, g, f, &x, &y);
draw_text(dr, x, y,
#define NPHASES lenof(loopy_line_redraw_phases)
static void game_redraw_line(drawing *dr, game_drawstate *ds,
- game_state *state, int i, int phase)
+ const game_state *state, int i, int phase)
{
grid *g = state->game_grid;
grid_edge *e = g->edges + i;
}
static void game_redraw_dot(drawing *dr, game_drawstate *ds,
- game_state *state, int i)
+ const game_state *state, int i)
{
grid *g = state->game_grid;
grid_dot *d = g->dots + i;
}
static void game_redraw_in_rect(drawing *dr, game_drawstate *ds,
- game_state *state, int x, int y, int w, int h)
+ const game_state *state,
+ int x, int y, int w, int h)
{
grid *g = state->game_grid;
int i, phase;
draw_update(dr, x, y, w, h);
}
-static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate,
- game_state *state, int dir, game_ui *ui,
+static void game_redraw(drawing *dr, game_drawstate *ds,
+ const game_state *oldstate, const game_state *state,
+ int dir, const game_ui *ui,
float animtime, float flashtime)
{
#define REDRAW_OBJECTS_LIMIT 16 /* Somewhat arbitrary tradeoff */
for (i = 0; i < g->num_faces; i++) {
grid_face *f = g->faces + i;
int sides = f->order;
+ int yes_order, no_order;
int clue_mistake;
int clue_satisfied;
int n = state->clues[i];
if (n < 0)
continue;
- clue_mistake = (face_order(state, i, LINE_YES) > n ||
- face_order(state, i, LINE_NO ) > (sides-n));
- clue_satisfied = (face_order(state, i, LINE_YES) == n &&
- face_order(state, i, LINE_NO ) == (sides-n));
+ yes_order = face_order(state, i, LINE_YES);
+ if (state->exactly_one_loop) {
+ /*
+ * Special case: if the set of LINE_YES edges in the grid
+ * consists of exactly one loop and nothing else, then we
+ * switch to treating LINE_UNKNOWN the same as LINE_NO for
+ * purposes of clue checking.
+ *
+ * This is because some people like to play Loopy without
+ * using the right-click, i.e. never setting anything to
+ * LINE_NO. Without this special case, if a person playing
+ * in that style fills in what they think is a correct
+ * solution loop but in fact it has an underfilled clue,
+ * then we will display no victory flash and also no error
+ * highlight explaining why not. With this special case,
+ * we light up underfilled clues at the instant the loop
+ * is closed. (Of course, *overfilled* clues are fine
+ * either way.)
+ *
+ * (It might still be considered unfortunate that we can't
+ * warn this style of player any earlier, if they make a
+ * mistake very near the beginning which doesn't show up
+ * until they close the last edge of the loop. One other
+ * thing we _could_ do here is to treat any LINE_UNKNOWN
+ * as LINE_NO if either of its endpoints has yes-degree 2,
+ * reflecting the fact that setting that line to YES would
+ * be an obvious error. But I don't think even that could
+ * catch _all_ clue errors in a timely manner; I think
+ * there are some that won't be displayed until the loop
+ * is filled in, even so, and there's no way to avoid that
+ * with complete reliability except to switch to being a
+ * player who sets things to LINE_NO.)
+ */
+ no_order = sides - yes_order;
+ } else {
+ no_order = face_order(state, i, LINE_NO);
+ }
+
+ clue_mistake = (yes_order > n || no_order > (sides-n));
+ clue_satisfied = (yes_order == n && no_order == (sides-n));
if (clue_mistake != ds->clue_error[i] ||
clue_satisfied != ds->clue_satisfied[i]) {
ds->started = TRUE;
}
-static float game_flash_length(game_state *oldstate, game_state *newstate,
- int dir, game_ui *ui)
+static float game_flash_length(const game_state *oldstate,
+ const game_state *newstate, int dir, game_ui *ui)
{
if (!oldstate->solved && newstate->solved &&
!oldstate->cheated && !newstate->cheated) {
return 0.0F;
}
-static int game_status(game_state *state)
+static int game_status(const game_state *state)
{
return state->solved ? +1 : 0;
}
-static void game_print_size(game_params *params, float *x, float *y)
+static void game_print_size(const game_params *params, float *x, float *y)
{
int pw, ph;
*y = ph / 100.0F;
}
-static void game_print(drawing *dr, game_state *state, int tilesize)
+static void game_print(drawing *dr, const game_state *state, int tilesize)
{
int ink = print_mono_colour(dr, 0);
int i;
grid_face *f = g->faces + i;
int clue = state->clues[i];
if (clue >= 0) {
- char c[2];
+ char c[20];
int x, y;
- c[0] = CLUE2CHAR(clue);
- c[1] = '\0';
+ sprintf(c, "%d", state->clues[i]);
face_text_pos(ds, g, f, &x, &y);
draw_text(dr, x, y,
FONT_VARIABLE, ds->tilesize / 2,