X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=blobdiff_plain;f=loopy.c;h=92b27ab51690f10aa89983b8af934fa591dc1449;hb=db313b3948d27244dd7c34c2609c66d6204d8931;hp=136b9e9fa16c4ab1c8696b731b0b1accb4f0126c;hpb=24848706edfdd1db1f97e3681d7ff52bec2fa575;p=sgt-puzzles.git diff --git a/loopy.c b/loopy.c index 136b9e9..92b27ab 100644 --- a/loopy.c +++ b/loopy.c @@ -118,6 +118,7 @@ struct game_state { char *lines; unsigned char *line_errors; + int exactly_one_loop; int solved; int cheated; @@ -242,32 +243,55 @@ static void check_caches(const solver_state* sstate); #define check_caches(s) #endif -/* ------- List of grid generators ------- */ -#define GRIDLIST(A) \ - A(Squares,GRID_SQUARE,3,3) \ - A(Triangular,GRID_TRIANGULAR,3,3) \ - A(Honeycomb,GRID_HONEYCOMB,3,3) \ - A(Snub-Square,GRID_SNUBSQUARE,3,3) \ - A(Cairo,GRID_CAIRO,3,4) \ - A(Great-Hexagonal,GRID_GREATHEXAGONAL,3,3) \ - A(Octagonal,GRID_OCTAGONAL,3,3) \ - A(Kites,GRID_KITE,3,3) \ - A(Floret,GRID_FLORET,1,2) \ - A(Dodecagonal,GRID_DODECAGONAL,2,2) \ - A(Great-Dodecagonal,GRID_GREATDODECAGONAL,2,2) \ - A(Penrose (kite/dart),GRID_PENROSE_P2,3,3) \ - A(Penrose (rhombs),GRID_PENROSE_P3,3,3) - -#define GRID_NAME(title,type,amin,omin) #title, -#define GRID_CONFIG(title,type,amin,omin) ":" #title -#define GRID_TYPE(title,type,amin,omin) type, +/* + * Grid type config options available in Loopy. + * + * Annoyingly, we have to use an enum here which doesn't match up + * exactly to the grid-type enum in grid.h. Values in params->types + * are given by names such as LOOPY_GRID_SQUARE, which shouldn't be + * confused with GRID_SQUARE which is the value you pass to grid_new() + * and friends. So beware! + * + * (This is partly for historical reasons - Loopy's version of the + * enum is encoded in game parameter strings, so we keep it for + * backwards compatibility. But also, we need to store additional data + * here alongside each enum value, such as names for the presets menu, + * which isn't stored in grid.h; so we have to have our own list macro + * here anyway, and C doesn't make it easy to enforce that that lines + * up exactly with grid.h.) + * + * Do not add values to this list _except_ at the end, or old game ids + * will stop working! + */ +#define GRIDLIST(A) \ + A("Squares",SQUARE,3,3) \ + A("Triangular",TRIANGULAR,3,3) \ + A("Honeycomb",HONEYCOMB,3,3) \ + A("Snub-Square",SNUBSQUARE,3,3) \ + A("Cairo",CAIRO,3,4) \ + A("Great-Hexagonal",GREATHEXAGONAL,3,3) \ + A("Octagonal",OCTAGONAL,3,3) \ + A("Kites",KITE,3,3) \ + A("Floret",FLORET,1,2) \ + A("Dodecagonal",DODECAGONAL,2,2) \ + A("Great-Dodecagonal",GREATDODECAGONAL,2,2) \ + A("Penrose (kite/dart)",PENROSE_P2,3,3) \ + A("Penrose (rhombs)",PENROSE_P3,3,3) \ + A("Great-Great-Dodecagonal",GREATGREATDODECAGONAL,2,2) \ + /* end of list */ + +#define GRID_NAME(title,type,amin,omin) title, +#define GRID_CONFIG(title,type,amin,omin) ":" title +#define GRID_LOOPYTYPE(title,type,amin,omin) LOOPY_GRID_ ## type, +#define GRID_GRIDTYPE(title,type,amin,omin) GRID_ ## type, #define GRID_SIZES(title,type,amin,omin) \ {amin, omin, \ "Width and height for this grid type must both be at least " #amin, \ "At least one of width and height for this grid type must be at least " #omin,}, +enum { GRIDLIST(GRID_LOOPYTYPE) LOOPY_GRID_DUMMY_TERMINATOR }; static char const *const gridnames[] = { GRIDLIST(GRID_NAME) }; #define GRID_CONFIGS GRIDLIST(GRID_CONFIG) -static grid_type grid_types[] = { GRIDLIST(GRID_TYPE) }; +static grid_type grid_types[] = { GRIDLIST(GRID_GRIDTYPE) }; #define NUM_GRID_TYPES (sizeof(grid_types) / sizeof(grid_types[0])) static const struct { int amin, omin; @@ -325,6 +349,7 @@ static game_state *dup_game(const game_state *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; @@ -488,61 +513,82 @@ static game_params *dup_params(const game_params *params) return ret; } -static const game_params presets[] = { +static const game_params loopy_presets_top[] = { #ifdef SMALL_SCREEN - { 7, 7, DIFF_EASY, 0 }, - { 7, 7, DIFF_NORMAL, 0 }, - { 7, 7, DIFF_HARD, 0 }, - { 7, 7, DIFF_HARD, 1 }, - { 7, 7, DIFF_HARD, 2 }, - { 5, 5, DIFF_HARD, 3 }, - { 7, 7, DIFF_HARD, 4 }, - { 5, 4, DIFF_HARD, 5 }, - { 5, 5, DIFF_HARD, 6 }, - { 5, 5, DIFF_HARD, 7 }, - { 3, 3, DIFF_HARD, 8 }, - { 3, 3, DIFF_HARD, 9 }, - { 3, 3, DIFF_HARD, 10 }, - { 6, 6, DIFF_HARD, 11 }, - { 6, 6, DIFF_HARD, 12 }, + { 7, 7, DIFF_EASY, LOOPY_GRID_SQUARE }, + { 7, 7, DIFF_NORMAL, LOOPY_GRID_SQUARE }, + { 7, 7, DIFF_HARD, LOOPY_GRID_SQUARE }, + { 7, 7, DIFF_HARD, LOOPY_GRID_TRIANGULAR }, + { 5, 5, DIFF_HARD, LOOPY_GRID_SNUBSQUARE }, + { 7, 7, DIFF_HARD, LOOPY_GRID_CAIRO }, + { 5, 5, DIFF_HARD, LOOPY_GRID_KITE }, + { 6, 6, DIFF_HARD, LOOPY_GRID_PENROSE_P2 }, + { 6, 6, DIFF_HARD, LOOPY_GRID_PENROSE_P3 }, #else - { 7, 7, DIFF_EASY, 0 }, - { 10, 10, DIFF_EASY, 0 }, - { 7, 7, DIFF_NORMAL, 0 }, - { 10, 10, DIFF_NORMAL, 0 }, - { 7, 7, DIFF_HARD, 0 }, - { 10, 10, DIFF_HARD, 0 }, - { 10, 10, DIFF_HARD, 1 }, - { 12, 10, DIFF_HARD, 2 }, - { 7, 7, DIFF_HARD, 3 }, - { 9, 9, DIFF_HARD, 4 }, - { 5, 4, DIFF_HARD, 5 }, - { 7, 7, DIFF_HARD, 6 }, - { 5, 5, DIFF_HARD, 7 }, - { 5, 5, DIFF_HARD, 8 }, - { 5, 4, DIFF_HARD, 9 }, - { 5, 4, DIFF_HARD, 10 }, - { 10, 10, DIFF_HARD, 11 }, - { 10, 10, DIFF_HARD, 12 } + { 7, 7, DIFF_EASY, LOOPY_GRID_SQUARE }, + { 10, 10, DIFF_EASY, LOOPY_GRID_SQUARE }, + { 7, 7, DIFF_NORMAL, LOOPY_GRID_SQUARE }, + { 10, 10, DIFF_NORMAL, LOOPY_GRID_SQUARE }, + { 7, 7, DIFF_HARD, LOOPY_GRID_SQUARE }, + { 10, 10, DIFF_HARD, LOOPY_GRID_SQUARE }, + { 12, 10, DIFF_HARD, LOOPY_GRID_TRIANGULAR }, + { 7, 7, DIFF_HARD, LOOPY_GRID_SNUBSQUARE }, + { 9, 9, DIFF_HARD, LOOPY_GRID_CAIRO }, + { 5, 5, DIFF_HARD, LOOPY_GRID_KITE }, + { 10, 10, DIFF_HARD, LOOPY_GRID_PENROSE_P2 }, + { 10, 10, DIFF_HARD, LOOPY_GRID_PENROSE_P3 }, #endif }; -static int game_fetch_preset(int i, char **name, game_params **params) +static const game_params loopy_presets_more[] = { +#ifdef SMALL_SCREEN + { 7, 7, DIFF_HARD, LOOPY_GRID_HONEYCOMB }, + { 5, 4, DIFF_HARD, LOOPY_GRID_GREATHEXAGONAL }, + { 5, 5, DIFF_HARD, LOOPY_GRID_OCTAGONAL }, + { 3, 3, DIFF_HARD, LOOPY_GRID_FLORET }, + { 3, 3, DIFF_HARD, LOOPY_GRID_DODECAGONAL }, + { 3, 3, DIFF_HARD, LOOPY_GRID_GREATDODECAGONAL }, + { 3, 2, DIFF_HARD, LOOPY_GRID_GREATGREATDODECAGONAL }, +#else + { 10, 10, DIFF_HARD, LOOPY_GRID_HONEYCOMB }, + { 5, 4, DIFF_HARD, LOOPY_GRID_GREATHEXAGONAL }, + { 7, 7, DIFF_HARD, LOOPY_GRID_OCTAGONAL }, + { 5, 5, DIFF_HARD, LOOPY_GRID_FLORET }, + { 5, 4, DIFF_HARD, LOOPY_GRID_DODECAGONAL }, + { 5, 4, DIFF_HARD, LOOPY_GRID_GREATDODECAGONAL }, + { 5, 3, DIFF_HARD, LOOPY_GRID_GREATGREATDODECAGONAL }, +#endif +}; + +static void preset_menu_add_preset_with_title(struct preset_menu *menu, + const game_params *params) { - game_params *tmppar; char buf[80]; + game_params *dup_params; - if (i < 0 || i >= lenof(presets)) - return FALSE; + sprintf(buf, "%dx%d %s - %s", params->h, params->w, + gridnames[params->type], diffnames[params->diff]); - tmppar = snew(game_params); - *tmppar = presets[i]; - *params = tmppar; - sprintf(buf, "%dx%d %s - %s", tmppar->h, tmppar->w, - gridnames[tmppar->type], diffnames[tmppar->diff]); - *name = dupstr(buf); + dup_params = snew(game_params); + *dup_params = *params; - return TRUE; + preset_menu_add_preset(menu, dupstr(buf), dup_params); +} + +static struct preset_menu *game_preset_menu(void) +{ + struct preset_menu *top, *more; + int i; + + top = preset_menu_new(); + for (i = 0; i < lenof(loopy_presets_top); i++) + preset_menu_add_preset_with_title(top, &loopy_presets_top[i]); + + more = preset_menu_add_submenu(top, dupstr("More...")); + for (i = 0; i < lenof(loopy_presets_more); i++) + preset_menu_add_preset_with_title(more, &loopy_presets_more[i]); + + return top; } static void free_params(game_params *params) @@ -1380,6 +1426,7 @@ static char *new_game_desc(const game_params *params, random_state *rs, 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; @@ -1451,6 +1498,7 @@ static game_state *new_game(midend *me, const game_params *params, 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; @@ -1491,7 +1539,7 @@ static int check_completion(game_state *state) grid *g = state->game_grid; int i, ret; int *dsf, *component_state; - int nsilly, nloop, npath, largest_comp, largest_size; + int nsilly, nloop, npath, largest_comp, largest_size, total_pathsize; enum { COMP_NONE, COMP_LOOP, COMP_PATH, COMP_SILLY, COMP_EMPTY }; memset(state->line_errors, 0, g->num_edges); @@ -1560,15 +1608,19 @@ static int check_completion(game_state *state) * hence they all consist of either a simple loop, or a simple * path with two endpoints. * - * - 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.) + * - For these purposes, group together all the paths and imagine + * them to be a single component (because in most normal + * situations the player will gradually build up the solution + * _not_ all in one connected segment, but as lots of separate + * little path pieces that gradually connect to each other). * - * - 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. + * - After doing that, if there is exactly one (sensible) + * component - be it a collection of paths or a loop - then + * highlight no further edge errors. (The former case is normal + * during play, and the latter is a potentially solved puzzle.) + * + * - Otherwise, find the largest of the sensible components, + * leave that one unhighlighted, and light the rest up in red. */ dsf = snew_dsf(g->num_dots); @@ -1636,18 +1688,18 @@ static int check_completion(game_state *state) * vertices in the grid data structure, which is fairly arbitrary * but at least stays stable throughout the game.) */ nsilly = nloop = npath = 0; + total_pathsize = 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) { + } else if (component_state[i] == COMP_PATH) { + total_pathsize += dsf_size(dsf, i); + npath = 1; + } else if (component_state[i] == COMP_LOOP) { int this_size; - if (component_state[i] == COMP_PATH) - npath++; - else if (component_state[i] == COMP_LOOP) - nloop++; + nloop++; if ((this_size = dsf_size(dsf, i)) > largest_size) { largest_comp = i; @@ -1655,6 +1707,10 @@ static int check_completion(game_state *state) } } } + if (largest_size < total_pathsize) { + largest_comp = -1; /* means the paths */ + largest_size = total_pathsize; + } if (nloop > 0 && nloop + npath > 1) { /* @@ -1667,8 +1723,10 @@ static int check_completion(game_state *state) 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) + if ((component_state[comp] == COMP_PATH && + -1 != largest_comp) || + (component_state[comp] == COMP_LOOP && + comp != largest_comp)) state->line_errors[i] = TRUE; } } @@ -1688,8 +1746,17 @@ static int check_completion(game_state *state) 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); @@ -2880,7 +2947,8 @@ static char *interpret_move(const game_state *state, game_ui *ui, grid *g = state->game_grid; grid_edge *e; int i; - char *ret, buf[80]; + char *movebuf; + int movelen, movesize; char button_char = ' '; enum line_state old_state; @@ -2942,11 +3010,83 @@ static char *interpret_move(const game_state *state, game_ui *ui, return NULL; } + movelen = 0; + 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; + } - sprintf(buf, "%d%c", i, (int)button_char); - ret = dupstr(buf); + 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; + + 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++; + } + } - return ret; + 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; + } + + 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:; + } + } + + return sresize(movebuf, movelen+1, char); } static game_state *execute_move(const game_state *state, const char *move) @@ -3263,16 +3403,53 @@ static void game_redraw(drawing *dr, game_drawstate *ds, 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]) { @@ -3463,7 +3640,7 @@ static void game_print(drawing *dr, const game_state *state, int tilesize) const struct game thegame = { "Loopy", "games.loopy", "loopy", default_params, - game_fetch_preset, + NULL, game_preset_menu, decode_params, encode_params, free_params,