diff --git a/puzzles.but b/puzzles.but index 7d90f43..5f8bff4 100644 --- a/puzzles.but +++ b/puzzles.but @@ -415,9 +415,41 @@ Right-clicking will move it in the opposite direction. \H{sixteen-params} \I{parameters, for Sixteen}Sixteen parameters -The only parameters available from the \q{Custom...} option on the -\q{Type} menu are \e{Width} and \e{Height}, which are -self-explanatory. +The parameters available from the \q{Custom...} option on the +\q{Type} menu are: + +\b \e{Width} and \e{Height}, which are self-explanatory. + +\b You can ask for a limited shuffling operation to be performed on +the grid. By default, Sixteen will shuffle the grid in such a way +that any arrangement is about as probable as any other. You can +override this by requesting a precise number of shuffling moves to +be performed. Typically your aim is then to determine the precise +set of shuffling moves and invert them exactly, so that you answer +(say) a four-move shuffle with a four-move solution. Note that the +more moves you ask for, the more likely it is that solutions shorter +than the target length will turn out to be possible. + +\H{sixteen-cmdline} \I{command line, for Sixteen}Additional +command-line configuration + +The limited shuffle parameter, described in \k{sixteen-params}, is +not mentioned by default in the game ID (see \k{common-id}). So if +you set your shuffling move count to (say) 4, and then you generate +a normal 4\by\.4 grid, then the game ID will simply say +\c{4x4:}\e{numbers}. This means that if you send the game ID to +another player and they paste it into their copy of Sixteen, their +game will not be automatically configured to use the same shuffle +limit in any subsequent grids it generates. (I don't think the +average person examining a single grid sent to them by another +player would want their configuration modified to that extent.) + +If you are specifying a game ID or game parameters on the command +line (see \k{common-cmdline}) and you do want to configure the +shuffle limit, you can do it by suffixing the letter \cq{m} to the +parameters, followed by the move count as a decimal number. For +example, \cq{sixteen 4x4m4} will start up Sixteen with a problem +guaranteed to be soluble in four moves or fewer. \C{twiddle} \i{Twiddle} @@ -475,6 +507,36 @@ you ask for an orientable puzzle, each tile will have a triangle drawn in it. All the triangles must be pointing upwards to complete the puzzle. +\b You can ask for a limited shuffling operation to be performed on +the grid. By default, Twiddle will shuffle the grid so much that any +arrangement is about as probable as any other. You can override this +by requesting a precise number of shuffling moves to be performed. +Typically your aim is then to determine the precise set of shuffling +moves and invert them exactly, so that you answer (say) a four-move +shuffle with a four-move solution. Note that the more moves you ask +for, the more likely it is that solutions shorter than the target +length will turn out to be possible. + +\H{twiddle-cmdline} \I{command line, for Twiddle}Additional +command-line configuration + +The limited shuffle parameter, described in \k{twiddle-parameters}, +is not mentioned by default in the game ID (see \k{common-id}). So +if you set your shuffling move count to (say) 4, and then you +generate a normal 3\by\.3 grid, then the game ID will simply say +\c{3x3n2:}\e{numbers}. This means that if you send the game ID to +another player and they paste it into their copy of Twiddle, their +game will not be automatically configured to use the same shuffle +limit in any subsequent grids it generates. (I don't think the +average person examining a single grid sent to them by another +player would want their configuration modified to that extent.) + +If you are specifying a game ID or game parameters on the command +line (see \k{common-cmdline}) and you do want to configure the +shuffle limit, you can do it by suffixing the letter \cq{m} to the +parameters, followed by the move count as a decimal number. For +example, \cq{twiddle 3x3n2m4} will start up Twiddle with a problem +guaranteed to be soluble in four moves or fewer. \C{rectangles} \i{Rectangles} diff --git a/sixteen.c b/sixteen.c index 3a5ce8a..2751be4 100644 --- a/sixteen.c +++ b/sixteen.c @@ -36,6 +36,7 @@ enum { struct game_params { int w, h; + int movetarget; }; struct game_state { @@ -44,7 +45,7 @@ struct game_state { int completed; int just_used_solve; /* used to suppress undo animation */ int used_solve; /* used to suppress completion flash */ - int movecount; + int movecount, movetarget; int last_movement_sense; }; @@ -53,6 +54,7 @@ static game_params *default_params(void) game_params *ret = snew(game_params); ret->w = ret->h = 4; + ret->movetarget = 0; return ret; } @@ -77,6 +79,7 @@ static int game_fetch_preset(int i, char **name, game_params **params) *params = ret = snew(game_params); ret->w = w; ret->h = h; + ret->movetarget = 0; return TRUE; } @@ -101,6 +104,14 @@ static game_params *decode_params(char const *string) if (*string == 'x') { string++; ret->h = atoi(string); + while (*string && isdigit((unsigned char)*string)) + string++; + } + if (*string == 'm') { + string++; + ret->movetarget = atoi(string); + while (*string && isdigit((unsigned char)*string)) + string++; } return ret; @@ -120,7 +131,7 @@ static config_item *game_configure(game_params *params) config_item *ret; char buf[80]; - ret = snewn(3, config_item); + ret = snewn(4, config_item); ret[0].name = "Width"; ret[0].type = C_STRING; @@ -134,11 +145,17 @@ static config_item *game_configure(game_params *params) ret[1].sval = dupstr(buf); ret[1].ival = 0; - ret[2].name = NULL; - ret[2].type = C_END; - ret[2].sval = NULL; + ret[2].name = "Number of shuffling moves"; + ret[2].type = C_STRING; + sprintf(buf, "%d", params->movetarget); + ret[2].sval = dupstr(buf); ret[2].ival = 0; + ret[3].name = NULL; + ret[3].type = C_END; + ret[3].sval = NULL; + ret[3].ival = 0; + return ret; } @@ -148,6 +165,7 @@ static game_params *custom_params(config_item *cfg) ret->w = atoi(cfg[0].sval); ret->h = atoi(cfg[1].sval); + ret->movetarget = atoi(cfg[2].sval); return ret; } @@ -186,75 +204,161 @@ static char *new_game_seed(game_params *params, random_state *rs, n = params->w * params->h; tiles = snewn(n, int); - used = snewn(n, int); - for (i = 0; i < n; i++) { - tiles[i] = -1; - used[i] = FALSE; - } + if (params->movetarget) { + int prevstart = -1, prevoffset = -1, prevdirection = 0, nrepeats = 0; - /* - * If both dimensions are odd, there is a parity constraint. - */ - if (params->w & params->h & 1) - stop = 2; - else - stop = 0; + /* + * Shuffle the old-fashioned way, by making a series of + * single moves on the grid. + */ - /* - * Place everything except (possibly) the last two tiles. - */ - for (x = 0, i = n; i > stop; i--) { - int k = i > 1 ? random_upto(rs, i) : 0; - int j; + for (i = 0; i < n; i++) + tiles[i] = i; - for (j = 0; j < n; j++) - if (!used[j] && (k-- == 0)) - break; + for (i = 0; i < params->movetarget; i++) { + int start, offset, len, direction; + int j, tmp; - assert(j < n && !used[j]); - used[j] = TRUE; + /* + * Choose a move to make. We can choose from any row + * or any column. + */ + while (1) { + j = random_upto(rs, params->w + params->h); - while (tiles[x] >= 0) - x++; - assert(x < n); - tiles[x] = j; - } + if (j < params->w) { + /* Column. */ + start = j; + offset = params->w; + len = params->h; + } else { + /* Row. */ + start = (j - params->w) * params->w; + offset = 1; + len = params->w; + } - if (stop) { - /* - * Find the last two locations, and the last two pieces. - */ - while (tiles[x] >= 0) - x++; - assert(x < n); - x1 = x; - x++; - while (tiles[x] >= 0) - x++; - assert(x < n); - x2 = x; + direction = -1 + 2 * random_upto(rs, 2); - for (i = 0; i < n; i++) - if (!used[i]) - break; - p1 = i; - for (i = p1+1; i < n; i++) - if (!used[i]) - break; - p2 = i; + /* + * To at least _try_ to avoid boring cases, check that + * this move doesn't directly undo the previous one, or + * repeat it so many times as to turn it into fewer + * moves. + */ + if (start == prevstart && offset == prevoffset) { + if (direction == -prevdirection) + continue; /* inverse of previous move */ + else if (2 * (nrepeats+1) > len) + continue; /* previous move repeated too often */ + } - /* - * Try the last two tiles one way round. If that fails, swap - * them. - */ - tiles[x1] = p1; - tiles[x2] = p2; - if (perm_parity(tiles, n) != 0) { - tiles[x1] = p2; - tiles[x2] = p1; - assert(perm_parity(tiles, n) == 0); - } + /* If we didn't `continue', we've found an OK move to make. */ + break; + } + + /* + * Now save the move into the `prev' variables. + */ + if (start == prevstart && offset == prevoffset) { + nrepeats++; + } else { + prevstart = start; + prevoffset = offset; + prevdirection = direction; + nrepeats = 1; + } + + /* + * And make it. + */ + if (direction < 0) { + start += (len-1) * offset; + offset = -offset; + } + tmp = tiles[start]; + for (j = 0; j+1 < len; j++) + tiles[start + j*offset] = tiles[start + (j+1)*offset]; + tiles[start + (len-1) * offset] = tmp; + } + + } else { + + used = snewn(n, int); + + for (i = 0; i < n; i++) { + tiles[i] = -1; + used[i] = FALSE; + } + + /* + * If both dimensions are odd, there is a parity + * constraint. + */ + if (params->w & params->h & 1) + stop = 2; + else + stop = 0; + + /* + * Place everything except (possibly) the last two tiles. + */ + for (x = 0, i = n; i > stop; i--) { + int k = i > 1 ? random_upto(rs, i) : 0; + int j; + + for (j = 0; j < n; j++) + if (!used[j] && (k-- == 0)) + break; + + assert(j < n && !used[j]); + used[j] = TRUE; + + while (tiles[x] >= 0) + x++; + assert(x < n); + tiles[x] = j; + } + + if (stop) { + /* + * Find the last two locations, and the last two + * pieces. + */ + while (tiles[x] >= 0) + x++; + assert(x < n); + x1 = x; + x++; + while (tiles[x] >= 0) + x++; + assert(x < n); + x2 = x; + + for (i = 0; i < n; i++) + if (!used[i]) + break; + p1 = i; + for (i = p1+1; i < n; i++) + if (!used[i]) + break; + p2 = i; + + /* + * Try the last two tiles one way round. If that fails, + * swap them. + */ + tiles[x1] = p1; + tiles[x2] = p2; + if (perm_parity(tiles, n) != 0) { + tiles[x1] = p2; + tiles[x2] = p1; + assert(perm_parity(tiles, n) == 0); + } + } + + sfree(used); } /* @@ -276,7 +380,6 @@ static char *new_game_seed(game_params *params, random_state *rs, ret[retlen-1] = '\0'; /* delete last comma */ sfree(tiles); - sfree(used); return ret; } @@ -361,6 +464,7 @@ static game_state *new_game(game_params *params, char *seed) assert(!*p); state->completed = state->movecount = 0; + state->movetarget = params->movetarget; state->used_solve = state->just_used_solve = FALSE; state->last_movement_sense = 0; @@ -378,6 +482,7 @@ static game_state *dup_game(game_state *state) memcpy(ret->tiles, state->tiles, state->w * state->h * sizeof(int)); ret->completed = state->completed; ret->movecount = state->movecount; + ret->movetarget = state->movetarget; ret->used_solve = state->used_solve; ret->just_used_solve = state->just_used_solve; ret->last_movement_sense = state->last_movement_sense; @@ -816,10 +921,14 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, if (state->used_solve) sprintf(statusbuf, "Moves since auto-solve: %d", state->movecount - state->completed); - else + else { sprintf(statusbuf, "%sMoves: %d", (state->completed ? "COMPLETED! " : ""), (state->completed ? state->completed : state->movecount)); + if (state->movetarget) + sprintf(statusbuf+strlen(statusbuf), " (target %d)", + state->movetarget); + } status_bar(fe, statusbuf); } diff --git a/twiddle.c b/twiddle.c index 589dbe6..707eab7 100644 --- a/twiddle.c +++ b/twiddle.c @@ -39,6 +39,7 @@ struct game_params { int w, h, n; int rowsonly; int orientable; + int movetarget; }; struct game_state { @@ -48,7 +49,7 @@ struct game_state { int completed; int just_used_solve; /* used to suppress undo animation */ int used_solve; /* used to suppress completion flash */ - int movecount; + int movecount, movetarget; int lastx, lasty, lastr; /* coordinates of last rotation */ }; @@ -59,6 +60,7 @@ static game_params *default_params(void) ret->w = ret->h = 3; ret->n = 2; ret->rowsonly = ret->orientable = FALSE; + ret->movetarget = 0; return ret; } @@ -108,6 +110,7 @@ static game_params *decode_params(char const *string) ret->w = ret->h = atoi(string); ret->n = 2; ret->rowsonly = ret->orientable = FALSE; + ret->movetarget = 0; while (*string && isdigit(*string)) string++; if (*string == 'x') { string++; @@ -124,6 +127,10 @@ static game_params *decode_params(char const *string) ret->rowsonly = TRUE; } else if (*string == 'o') { ret->orientable = TRUE; + } else if (*string == 'm') { + string++; + ret->movetarget = atoi(string); + while (string[1] && isdigit(string[1])) string++; } string++; } @@ -145,7 +152,7 @@ static config_item *game_configure(game_params *params) config_item *ret; char buf[80]; - ret = snewn(6, config_item); + ret = snewn(7, config_item); ret[0].name = "Width"; ret[0].type = C_STRING; @@ -175,11 +182,17 @@ static config_item *game_configure(game_params *params) ret[4].sval = NULL; ret[4].ival = params->orientable; - ret[5].name = NULL; - ret[5].type = C_END; - ret[5].sval = NULL; + ret[5].name = "Number of shuffling moves"; + ret[5].type = C_STRING; + sprintf(buf, "%d", params->movetarget); + ret[5].sval = dupstr(buf); ret[5].ival = 0; + ret[6].name = NULL; + ret[6].type = C_END; + ret[6].sval = NULL; + ret[6].ival = 0; + return ret; } @@ -192,6 +205,7 @@ static game_params *custom_params(config_item *cfg) ret->n = atoi(cfg[2].sval); ret->rowsonly = cfg[3].ival; ret->orientable = cfg[4].ival; + ret->movetarget = atoi(cfg[5].sval); return ret; } @@ -317,23 +331,41 @@ static char *new_game_seed(game_params *params, random_state *rs, * and simply shuffle the grid by making a long sequence of * randomly chosen moves. */ - total_moves = w*h*n*n*2 + random_upto(rs, 1); - for (i = 0; i < total_moves; i++) { - int x, y; + total_moves = params->movetarget; + if (!total_moves) + total_moves = w*h*n*n*2 + random_upto(rs, 2); - x = random_upto(rs, w - n + 1); - y = random_upto(rs, h - n + 1); - do_rotate(grid, w, h, n, params->orientable, - x, y, 1 + random_upto(rs, 3)); + do { + int oldx = -1, oldy = -1, oldr = -1; - /* - * Optionally one more move in case the entire grid has - * happened to come out solved. - */ - if (i == total_moves - 1 && grid_complete(grid, wh, - params->orientable)) - i--; - } + for (i = 0; i < total_moves; i++) { + int x, y, r; + + do { + x = random_upto(rs, w - n + 1); + y = random_upto(rs, h - n + 1); + r = 1 + 2 * random_upto(rs, 2); + } while (x == oldx && y == oldy && (oldr == 0 || r == oldr)); + + do_rotate(grid, w, h, n, params->orientable, + x, y, r); + + /* + * Prevent immediate reversal of a previous move, or + * execution of three consecutive identical moves + * adding up to a single inverse move. One exception is + * when we only _have_ one x,y setting. + */ + if (w != n || h != n) { + if (oldx == x && oldy == y) + oldr = 0; /* now avoid _any_ move in this x,y */ + else + oldr = -r & 3; /* only prohibit the exact inverse */ + oldx = x; + oldy = y; + } + } + } while (grid_complete(grid, wh, params->orientable)); /* * Now construct the game seed, by describing the grid as a @@ -410,6 +442,7 @@ static game_state *new_game(game_params *params, char *seed) state->completed = 0; state->used_solve = state->just_used_solve = FALSE; state->movecount = 0; + state->movetarget = params->movetarget; state->lastx = state->lasty = state->lastr = -1; state->grid = snewn(wh, int); @@ -445,6 +478,7 @@ static game_state *dup_game(game_state *state) ret->orientable = state->orientable; ret->completed = state->completed; ret->movecount = state->movecount; + ret->movetarget = state->movetarget; ret->lastx = state->lastx; ret->lasty = state->lasty; ret->lastr = state->lastr; @@ -1021,10 +1055,14 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, if (state->used_solve) sprintf(statusbuf, "Moves since auto-solve: %d", state->movecount - state->completed); - else + else { sprintf(statusbuf, "%sMoves: %d", (state->completed ? "COMPLETED! " : ""), (state->completed ? state->completed : state->movecount)); + if (state->movetarget) + sprintf(statusbuf+strlen(statusbuf), " (target %d)", + state->movetarget); + } status_bar(fe, statusbuf); }