From 9038fd1115660daacea090db86a558cc9ce4ed75 Mon Sep 17 00:00:00 2001 From: simon Date: Sat, 30 Apr 2005 12:54:22 +0000 Subject: [PATCH] New puzzle: `twiddle', generalised from a random door-unlocking gadget in Metroid Prime 2. git-svn-id: svn://svn.tartarus.org/sgt/puzzles@5708 cda61777-01e9-0310-a592-d414129be87e --- Recipe | 4 +- list.c | 2 + puzzles.but | 51 ++++ twiddle.c | 867 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 923 insertions(+), 1 deletion(-) create mode 100644 twiddle.c diff --git a/Recipe b/Recipe index 8223861..fe6c003 100644 --- a/Recipe +++ b/Recipe @@ -18,7 +18,7 @@ COMMON = midend misc malloc random NET = net tree234 NETSLIDE = netslide tree234 -ALL = list NET NETSLIDE cube fifteen sixteen rect pattern solo +ALL = list NET NETSLIDE cube fifteen sixteen rect pattern solo twiddle net : [X] gtk COMMON NET netslide : [X] gtk COMMON NETSLIDE @@ -28,6 +28,7 @@ sixteen : [X] gtk COMMON sixteen rect : [X] gtk COMMON rect pattern : [X] gtk COMMON pattern solo : [X] gtk COMMON solo +twiddle : [X] gtk COMMON twiddle # The Windows Net shouldn't be called `net.exe' since Windows # already has a reasonably important utility program by that name! @@ -39,6 +40,7 @@ sixteen : [G] WINDOWS COMMON sixteen rect : [G] WINDOWS COMMON rect pattern : [G] WINDOWS COMMON pattern solo : [G] WINDOWS COMMON solo +twiddle : [G] WINDOWS COMMON twiddle # Mac OS X unified application containing all the puzzles. Puzzles : [MX] osx osx.icns osx-info.plist COMMON ALL diff --git a/list.c b/list.c index be494e8..6491934 100644 --- a/list.c +++ b/list.c @@ -25,6 +25,7 @@ extern const game pattern; extern const game rect; extern const game sixteen; extern const game solo; +extern const game twiddle; const game *gamelist[] = { &cube, @@ -35,6 +36,7 @@ const game *gamelist[] = { &rect, &sixteen, &solo, + &twiddle, }; const int gamecount = lenof(gamelist); diff --git a/puzzles.but b/puzzles.but index 8d592b5..b3317c3 100644 --- a/puzzles.but +++ b/puzzles.but @@ -382,6 +382,57 @@ The only parameters available from the \q{Custom...} option on the self-explanatory. +\C{twiddle} \i{Twiddle} + +\cfg{winhelp-topic}{games.twiddle} + +Twiddle is a tile-rearrangement puzzle, visually similar to Sixteen +(see \k{sixteen}): you are given a grid of square tiles, each +containing a number, and your aim is to arrange the numbers into +ascending order. + +In basic Twiddle, your move is to rotate a square group of four +tiles about their common centre. (Orientation is not significant: +tiles never end up upside down!) On more advanced settings, you can +rotate a larger square group of tiles. + +I first saw this type of puzzle in the GameCube game \q{Metroid +Prime 2}. In the Main Gyro Chamber in that game, there is a puzzle +you solve to unlock a door, which is a special case of Twiddle. I +developed this game as a generalisation of that puzzle. + +\H{twiddle-controls} \I{controls, for Twiddle}Twiddle controls + +To play Twiddle, click the mouse in the centre of the square group +you wish to rotate. In the basic mode, you rotate a 2\by\.2 square, +which means you have to click at a corner point where four tiles +meet. + +In more advanced modes you might be rotating 3\by\.3 or even more at +a time; if the size of the square is odd then you simply click in +the centre tile of the square you want to rotate. + +Clicking with the left mouse button rotates the group anticlockwise. +Clicking with the right button rotates it clockwise. + +(All the actions described in \k{common-actions} are also available.) + +\H{twiddle-parameters} \I{parameters, for Twiddle}Twiddle parameters + +Twiddle provides several configuration options via the \q{Custom} +option on the \q{Type} menu: + +\b You can configure the width and height of the puzzle grid. + +\b You can configure the size of square block that rotates at a time. + +\b You can ask for every square in the grid to be distinguishable +(the default), or you can ask for a simplified puzzle in which there +are groups of identical numbers. In the simplified puzzle your aim +is just to arrange all the 1s into the first row, all the 2s into +the second row, and so on. + + \C{rectangles} \i{Rectangles} \cfg{winhelp-topic}{games.rectangles} diff --git a/twiddle.c b/twiddle.c new file mode 100644 index 0000000..e7560f6 --- /dev/null +++ b/twiddle.c @@ -0,0 +1,867 @@ +/* + * twiddle.c: Puzzle involving rearranging a grid of squares by + * rotating subsquares. Adapted and generalised from a + * door-unlocking puzzle in Metroid Prime 2 (the one in the Main + * Gyro Chamber). + */ + +/* + * Possibly TODO: + * + * - it's horribly tempting to give the pieces significant + * _orientations_, perhaps by drawing some sort of oriented + * polygonal figure beneath the number. (An arrow pointing + * upwards springs readily to mind.) + */ + +#include +#include +#include +#include +#include +#include + +#include "puzzles.h" + +#define TILE_SIZE 48 +#define BORDER (TILE_SIZE / 2) +#define HIGHLIGHT_WIDTH (TILE_SIZE / 20) +#define COORD(x) ( (x) * TILE_SIZE + BORDER ) +#define FROMCOORD(x) ( ((x) - BORDER + TILE_SIZE) / TILE_SIZE - 1 ) + +#define PI 3.141592653589793238462643383279502884197169399 + +#define ANIM_PER_RADIUS_UNIT 0.13F +#define FLASH_FRAME 0.13F + +enum { + COL_BACKGROUND, + COL_TEXT, + COL_HIGHLIGHT, + COL_HIGHLIGHT_GENTLE, + COL_LOWLIGHT, + COL_LOWLIGHT_GENTLE, + NCOLOURS +}; + +struct game_params { + int w, h, n; + int rowsonly; +}; + +struct game_state { + int w, h, n; + int *grid; + int completed; + int movecount; + int lastx, lasty, lastr; /* coordinates of last rotation */ +}; + +static game_params *default_params(void) +{ + game_params *ret = snew(game_params); + + ret->w = ret->h = 3; + ret->n = 2; + ret->rowsonly = FALSE; + + return ret; +} + + +static void free_params(game_params *params) +{ + sfree(params); +} + +static game_params *dup_params(game_params *params) +{ + game_params *ret = snew(game_params); + *ret = *params; /* structure copy */ + return ret; +} + +static int game_fetch_preset(int i, char **name, game_params **params) +{ + static struct { + char *title; + game_params params; + } presets[] = { + { "3x3 rows only", { 3, 3, 2, TRUE } }, + { "3x3 normal", { 3, 3, 2, FALSE } }, + { "4x4 normal", { 4, 4, 2, FALSE } }, + { "4x4 radius 3", { 4, 4, 3, FALSE } }, + { "5x5 radius 3", { 5, 5, 3, FALSE } }, + { "6x6 radius 4", { 6, 6, 4, FALSE } }, + }; + + if (i < 0 || i >= lenof(presets)) + return FALSE; + + *name = dupstr(presets[i].title); + *params = dup_params(&presets[i].params); + + return TRUE; +} + +static game_params *decode_params(char const *string) +{ + game_params *ret = snew(game_params); + + ret->w = ret->h = atoi(string); + ret->n = 2; + ret->rowsonly = FALSE; + while (*string && isdigit(*string)) string++; + if (*string == 'x') { + string++; + ret->h = atoi(string); + while (*string && isdigit(*string)) string++; + } + if (*string == 'n') { + string++; + ret->n = atoi(string); + while (*string && isdigit(*string)) string++; + } + if (*string == 'r') { + string++; + ret->rowsonly = TRUE; + } + + return ret; +} + +static char *encode_params(game_params *params) +{ + char buf[256]; + sprintf(buf, "%dx%dn%d%s", params->w, params->h, params->n, + params->rowsonly ? "r" : ""); + return dupstr(buf); +} + +static config_item *game_configure(game_params *params) +{ + config_item *ret; + char buf[80]; + + ret = snewn(4, config_item); + + ret[0].name = "Width"; + ret[0].type = C_STRING; + sprintf(buf, "%d", params->w); + ret[0].sval = dupstr(buf); + ret[0].ival = 0; + + ret[1].name = "Height"; + ret[1].type = C_STRING; + sprintf(buf, "%d", params->h); + ret[1].sval = dupstr(buf); + ret[1].ival = 0; + + ret[2].name = "Rotation radius"; + ret[2].type = C_STRING; + sprintf(buf, "%d", params->n); + ret[2].sval = dupstr(buf); + ret[2].ival = 0; + + ret[3].name = "One number per row"; + ret[3].type = C_BOOLEAN; + ret[3].sval = NULL; + ret[3].ival = params->rowsonly; + + ret[4].name = NULL; + ret[4].type = C_END; + ret[4].sval = NULL; + ret[4].ival = 0; + + return ret; +} + +static game_params *custom_params(config_item *cfg) +{ + game_params *ret = snew(game_params); + + ret->w = atoi(cfg[0].sval); + ret->h = atoi(cfg[1].sval); + ret->n = atoi(cfg[2].sval); + ret->rowsonly = cfg[3].ival; + + return ret; +} + +static char *validate_params(game_params *params) +{ + if (params->n < 2) + return "Rotation radius must be at least two"; + if (params->w < params->n) + return "Width must be at least the rotation radius"; + if (params->h < params->n) + return "Height must be at least the rotation radius"; + return NULL; +} + +/* + * This function actually performs a rotation on a grid. The `x' + * and `y' coordinates passed in are the coordinates of the _top + * left corner_ of the rotated region. (Using the centre would have + * involved half-integers and been annoyingly fiddly. Clicking in + * the centre is good for a user interface, but too inconvenient to + * use internally.) + */ +static void do_rotate(int *grid, int w, int h, int n, int x, int y, int dir) +{ + int i, j; + + assert(x >= 0 && x+n <= w); + assert(y >= 0 && y+n <= h); + dir &= 3; + if (dir == 0) + return; /* nothing to do */ + + grid += y*w+x; /* translate region to top corner */ + + /* + * If we were leaving the result of the rotation in a separate + * grid, the simple thing to do would be to loop over each + * square within the rotated region and assign it from its + * source square. However, to do it in place without taking + * O(n^2) memory, we need to be marginally more clever. What + * I'm going to do is loop over about one _quarter_ of the + * rotated region and permute each element within that quarter + * with its rotational coset. + * + * The size of the region I need to loop over is (n+1)/2 by + * n/2, which is an obvious exact quarter for even n and is a + * rectangle for odd n. (For odd n, this technique leaves out + * one element of the square, which is of course the central + * one that never moves anyway.) + */ + for (i = 0; i < (n+1)/2; i++) { + for (j = 0; j < n/2; j++) { + int k; + int g[4]; + int p[4] = { + j*w+i, + i*w+(n-j-1), + (n-j-1)*w+(n-i-1), + (n-i-1)*w+j + }; + + for (k = 0; k < 4; k++) + g[k] = grid[p[k]]; + + for (k = 0; k < 4; k++) + grid[p[k]] = g[(k+dir) & 3]; + } + } +} + +static int grid_complete(int *grid, int wh) +{ + int ok = TRUE; + int i; + for (i = 1; i < wh; i++) + if (grid[i] < grid[i-1]) + ok = FALSE; + return ok; +} + +static char *new_game_seed(game_params *params, random_state *rs) +{ + int *grid; + int w = params->w, h = params->h, n = params->n, wh = w*h; + int i; + char *ret; + int retlen; + int total_moves; + + /* + * Set up a solved grid. + */ + grid = snewn(wh, int); + for (i = 0; i < wh; i++) + grid[i] = (params->rowsonly ? i/w : i) + 1; + + /* + * Shuffle it. This game is complex enough that I don't feel up + * to analysing its full symmetry properties (particularly at + * n=4 and above!), so I'm going to do it the pedestrian way + * and simply shuffle the grid by making a long sequence of + * randomly chosen moves. + */ + total_moves = w*h*n*n*2; + for (i = 0; i < total_moves; i++) { + int x, y; + + x = random_upto(rs, w - n + 1); + y = random_upto(rs, h - n + 1); + do_rotate(grid, w, h, n, x, y, 1 + random_upto(rs, 3)); + + /* + * Optionally one more move in case the entire grid has + * happened to come out solved. + */ + if (i == total_moves - 1 && grid_complete(grid, wh)) + i--; + } + + /* + * Now construct the game seed, by describing the grid as a + * simple sequence of comma-separated integers. + */ + ret = NULL; + retlen = 0; + for (i = 0; i < wh; i++) { + char buf[80]; + int k; + + k = sprintf(buf, "%d,", grid[i]); + + ret = sresize(ret, retlen + k + 1, char); + strcpy(ret + retlen, buf); + retlen += k; + } + ret[retlen-1] = '\0'; /* delete last comma */ + + sfree(grid); + return ret; +} + +static char *validate_seed(game_params *params, char *seed) +{ + char *p, *err; + int w = params->w, h = params->h, wh = w*h; + int i; + + p = seed; + err = NULL; + + for (i = 0; i < wh; i++) { + if (*p < '0' || *p > '9') { + return "Not enough numbers in string"; + } + while (*p >= '0' && *p <= '9') + p++; + if (i < wh-1 && *p != ',') { + return "Expected comma after number"; + } + else if (i == wh-1 && *p) { + return "Excess junk at end of string"; + } + + if (*p) p++; /* eat comma */ + } + + return NULL; +} + +static game_state *new_game(game_params *params, char *seed) +{ + game_state *state = snew(game_state); + int w = params->w, h = params->h, n = params->n, wh = w*h; + int i; + char *p; + + state->w = w; + state->h = h; + state->n = n; + state->completed = 0; + state->movecount = 0; + state->lastx = state->lasty = state->lastr = -1; + + state->grid = snewn(wh, int); + + p = seed; + + for (i = 0; i < wh; i++) { + state->grid[i] = atoi(p); + while (*p >= '0' && *p <= '9') + p++; + + if (*p) p++; /* eat comma */ + } + + return state; +} + +static game_state *dup_game(game_state *state) +{ + game_state *ret = snew(game_state); + + ret->w = state->w; + ret->h = state->h; + ret->n = state->n; + ret->completed = state->completed; + ret->movecount = state->movecount; + ret->lastx = state->lastx; + ret->lasty = state->lasty; + ret->lastr = state->lastr; + + ret->grid = snewn(ret->w * ret->h, int); + memcpy(ret->grid, state->grid, ret->w * ret->h * sizeof(int)); + + return ret; +} + +static void free_game(game_state *state) +{ + sfree(state->grid); + sfree(state); +} + +static game_ui *new_ui(game_state *state) +{ + return NULL; +} + +static void free_ui(game_ui *ui) +{ +} + +static game_state *make_move(game_state *from, game_ui *ui, int x, int y, + int button) +{ + int w = from->w, h = from->h, n = from->n, wh = w*h; + game_state *ret; + int dir; + + if (button == LEFT_BUTTON || button == RIGHT_BUTTON) { + /* + * Determine the coordinates of the click. We offset by n-1 + * half-blocks so that the user must click at the centre of + * a rotation region rather than at the corner. + */ + x -= (n-1) * TILE_SIZE / 2; + y -= (n-1) * TILE_SIZE / 2; + x = FROMCOORD(x); + y = FROMCOORD(y); + if (x < 0 || x > w-n || y < 0 || y > w-n) + return NULL; + + /* + * This is a valid move. Make it. + */ + ret = dup_game(from); + ret->movecount++; + dir = (button == LEFT_BUTTON ? 1 : -1); + do_rotate(ret->grid, w, h, n, x, y, dir); + ret->lastx = x; + ret->lasty = y; + ret->lastr = dir; + + /* + * See if the game has been completed. To do this we simply + * test that the grid contents are in increasing order. + */ + if (!ret->completed && grid_complete(ret->grid, wh)) + ret->completed = ret->movecount; + return ret; + } + return NULL; +} + +/* ---------------------------------------------------------------------- + * Drawing routines. + */ + +struct game_drawstate { + int started; + int w, h, bgcolour; + int *grid; +}; + +static void game_size(game_params *params, int *x, int *y) +{ + *x = TILE_SIZE * params->w + 2 * BORDER; + *y = TILE_SIZE * params->h + 2 * BORDER; +} + +static float *game_colours(frontend *fe, game_state *state, int *ncolours) +{ + float *ret = snewn(3 * NCOLOURS, float); + int i; + float max; + + frontend_default_colour(fe, &ret[COL_BACKGROUND * 3]); + + /* + * Drop the background colour so that the highlight is + * noticeably brighter than it while still being under 1. + */ + max = ret[COL_BACKGROUND*3]; + for (i = 1; i < 3; i++) + if (ret[COL_BACKGROUND*3+i] > max) + max = ret[COL_BACKGROUND*3+i]; + if (max * 1.2F > 1.0F) { + for (i = 0; i < 3; i++) + ret[COL_BACKGROUND*3+i] /= (max * 1.2F); + } + + for (i = 0; i < 3; i++) { + ret[COL_HIGHLIGHT * 3 + i] = ret[COL_BACKGROUND * 3 + i] * 1.2F; + ret[COL_HIGHLIGHT_GENTLE * 3 + i] = ret[COL_BACKGROUND * 3 + i] * 1.1F; + ret[COL_LOWLIGHT * 3 + i] = ret[COL_BACKGROUND * 3 + i] * 0.8F; + ret[COL_LOWLIGHT_GENTLE * 3 + i] = ret[COL_BACKGROUND * 3 + i] * 0.9F; + ret[COL_TEXT * 3 + i] = 0.0; + } + + *ncolours = NCOLOURS; + return ret; +} + +static game_drawstate *game_new_drawstate(game_state *state) +{ + struct game_drawstate *ds = snew(struct game_drawstate); + int i; + + ds->started = FALSE; + ds->w = state->w; + ds->h = state->h; + ds->bgcolour = COL_BACKGROUND; + ds->grid = snewn(ds->w*ds->h, int); + for (i = 0; i < ds->w*ds->h; i++) + ds->grid[i] = -1; + + return ds; +} + +static void game_free_drawstate(game_drawstate *ds) +{ + sfree(ds); +} + +struct rotation { + int cx, cy, cw, ch; /* clip region */ + int ox, oy; /* rotation origin */ + float c, s; /* cos and sin of rotation angle */ + int lc, rc, tc, bc; /* colours of tile edges */ +}; + +static void rotate(int *xy, struct rotation *rot) +{ + if (rot) { + float xf = xy[0] - rot->ox, yf = xy[1] - rot->oy; + float xf2, yf2; + + xf2 = rot->c * xf + rot->s * yf; + yf2 = - rot->s * xf + rot->c * yf; + + xy[0] = xf2 + rot->ox + 0.5; /* round to nearest */ + xy[1] = yf2 + rot->oy + 0.5; /* round to nearest */ + } +} + +static void draw_tile(frontend *fe, game_state *state, int x, int y, + int tile, int flash_colour, struct rotation *rot) +{ + int coords[8]; + char str[40]; + + if (rot) + clip(fe, rot->cx, rot->cy, rot->cw, rot->ch); + + /* + * We must draw each side of the tile's highlight separately, + * because in some cases (during rotation) they will all need + * to be different colours. + */ + + /* The centre point is common to all sides. */ + coords[4] = x + TILE_SIZE / 2; + coords[5] = y + TILE_SIZE / 2; + rotate(coords+4, rot); + + /* Right side. */ + coords[0] = x + TILE_SIZE - 1; + coords[1] = y + TILE_SIZE - 1; + rotate(coords+0, rot); + coords[2] = x + TILE_SIZE - 1; + coords[3] = y; + rotate(coords+2, rot); + draw_polygon(fe, coords, 3, TRUE, rot ? rot->rc : COL_LOWLIGHT); + draw_polygon(fe, coords, 3, FALSE, rot ? rot->rc : COL_LOWLIGHT); + + /* Bottom side. */ + coords[2] = x; + coords[3] = y + TILE_SIZE - 1; + rotate(coords+2, rot); + draw_polygon(fe, coords, 3, TRUE, rot ? rot->bc : COL_LOWLIGHT); + draw_polygon(fe, coords, 3, FALSE, rot ? rot->bc : COL_LOWLIGHT); + + /* Left side. */ + coords[0] = x; + coords[1] = y; + rotate(coords+0, rot); + draw_polygon(fe, coords, 3, TRUE, rot ? rot->lc : COL_HIGHLIGHT); + draw_polygon(fe, coords, 3, FALSE, rot ? rot->lc : COL_HIGHLIGHT); + + /* Top side. */ + coords[2] = x + TILE_SIZE - 1; + coords[3] = y; + rotate(coords+2, rot); + draw_polygon(fe, coords, 3, TRUE, rot ? rot->tc : COL_HIGHLIGHT); + draw_polygon(fe, coords, 3, FALSE, rot ? rot->tc : COL_HIGHLIGHT); + + if (rot) { + coords[0] = x + HIGHLIGHT_WIDTH; + coords[1] = y + HIGHLIGHT_WIDTH; + rotate(coords+0, rot); + coords[2] = x + HIGHLIGHT_WIDTH; + coords[3] = y + TILE_SIZE - 1 - HIGHLIGHT_WIDTH; + rotate(coords+2, rot); + coords[4] = x + TILE_SIZE - 1 - HIGHLIGHT_WIDTH; + coords[5] = y + TILE_SIZE - 1 - HIGHLIGHT_WIDTH; + rotate(coords+4, rot); + coords[6] = x + TILE_SIZE - 1 - HIGHLIGHT_WIDTH; + coords[7] = y + HIGHLIGHT_WIDTH; + rotate(coords+6, rot); + draw_polygon(fe, coords, 4, TRUE, flash_colour); + draw_polygon(fe, coords, 4, FALSE, flash_colour); + } else { + draw_rect(fe, x + HIGHLIGHT_WIDTH, y + HIGHLIGHT_WIDTH, + TILE_SIZE - 2*HIGHLIGHT_WIDTH, TILE_SIZE - 2*HIGHLIGHT_WIDTH, + flash_colour); + } + + coords[0] = x + TILE_SIZE/2; + coords[1] = y + TILE_SIZE/2; + rotate(coords+0, rot); + sprintf(str, "%d", tile); + draw_text(fe, coords[0], coords[1], + FONT_VARIABLE, TILE_SIZE/3, ALIGN_VCENTRE | ALIGN_HCENTRE, + COL_TEXT, str); + + if (rot) + unclip(fe); + + draw_update(fe, x, y, TILE_SIZE, TILE_SIZE); +} + +static int highlight_colour(float angle) +{ + int colours[32] = { + COL_LOWLIGHT, + COL_LOWLIGHT_GENTLE, + COL_LOWLIGHT_GENTLE, + COL_LOWLIGHT_GENTLE, + COL_HIGHLIGHT_GENTLE, + COL_HIGHLIGHT_GENTLE, + COL_HIGHLIGHT_GENTLE, + COL_HIGHLIGHT, + COL_HIGHLIGHT, + COL_HIGHLIGHT, + COL_HIGHLIGHT, + COL_HIGHLIGHT, + COL_HIGHLIGHT, + COL_HIGHLIGHT, + COL_HIGHLIGHT, + COL_HIGHLIGHT, + COL_HIGHLIGHT, + COL_HIGHLIGHT_GENTLE, + COL_HIGHLIGHT_GENTLE, + COL_HIGHLIGHT_GENTLE, + COL_LOWLIGHT_GENTLE, + COL_LOWLIGHT_GENTLE, + COL_LOWLIGHT_GENTLE, + COL_LOWLIGHT, + COL_LOWLIGHT, + COL_LOWLIGHT, + COL_LOWLIGHT, + COL_LOWLIGHT, + COL_LOWLIGHT, + COL_LOWLIGHT, + COL_LOWLIGHT, + COL_LOWLIGHT, + }; + + return colours[(int)((angle + 2*PI) / (PI/16)) & 31]; +} + +static float game_anim_length(game_state *oldstate, game_state *newstate, + int dir) +{ + return ANIM_PER_RADIUS_UNIT * sqrt(newstate->n-1); +} + +static float game_flash_length(game_state *oldstate, game_state *newstate, + int dir) +{ + if (!oldstate->completed && newstate->completed) + return 2 * FLASH_FRAME; + else + return 0.0F; +} + +static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, + game_state *state, int dir, game_ui *ui, + float animtime, float flashtime) +{ + int i, bgcolour; + struct rotation srot, *rot; + int lastx = -1, lasty = -1, lastr = -1; + + if (flashtime > 0) { + int frame = (int)(flashtime / FLASH_FRAME); + bgcolour = (frame % 2 ? COL_LOWLIGHT : COL_HIGHLIGHT); + } else + bgcolour = COL_BACKGROUND; + + if (!ds->started) { + int coords[6]; + + draw_rect(fe, 0, 0, + TILE_SIZE * state->w + 2 * BORDER, + TILE_SIZE * state->h + 2 * BORDER, COL_BACKGROUND); + draw_update(fe, 0, 0, + TILE_SIZE * state->w + 2 * BORDER, + TILE_SIZE * state->h + 2 * BORDER); + + /* + * Recessed area containing the whole puzzle. + */ + coords[0] = COORD(state->w) + HIGHLIGHT_WIDTH - 1; + coords[1] = COORD(state->h) + HIGHLIGHT_WIDTH - 1; + coords[2] = COORD(state->w) + HIGHLIGHT_WIDTH - 1; + coords[3] = COORD(0) - HIGHLIGHT_WIDTH; + coords[4] = COORD(0) - HIGHLIGHT_WIDTH; + coords[5] = COORD(state->h) + HIGHLIGHT_WIDTH - 1; + draw_polygon(fe, coords, 3, TRUE, COL_HIGHLIGHT); + draw_polygon(fe, coords, 3, FALSE, COL_HIGHLIGHT); + + coords[1] = COORD(0) - HIGHLIGHT_WIDTH; + coords[0] = COORD(0) - HIGHLIGHT_WIDTH; + draw_polygon(fe, coords, 3, TRUE, COL_LOWLIGHT); + draw_polygon(fe, coords, 3, FALSE, COL_LOWLIGHT); + + ds->started = TRUE; + } + + /* + * If we're drawing any rotated tiles, sort out the rotation + * parameters, and also zap the rotation region to the + * background colour before doing anything else. + */ + if (oldstate) { + float angle; + float anim_max = game_anim_length(oldstate, state, dir); + + if (dir > 0) { + lastx = state->lastx; + lasty = state->lasty; + lastr = state->lastr; + } else { + lastx = oldstate->lastx; + lasty = oldstate->lasty; + lastr = -oldstate->lastr; + } + + rot = &srot; + rot->cx = COORD(lastx); + rot->cy = COORD(lasty); + rot->cw = rot->ch = TILE_SIZE * state->n; + rot->ox = rot->cx + rot->cw/2; + rot->oy = rot->cy + rot->ch/2; + angle = (-PI/2 * lastr) * (1.0 - animtime / anim_max); + rot->c = cos(angle); + rot->s = sin(angle); + + /* + * Sort out the colours of the various sides of the tile. + */ + rot->lc = highlight_colour(PI + angle); + rot->rc = highlight_colour(angle); + rot->tc = highlight_colour(PI/2 + angle); + rot->bc = highlight_colour(-PI/2 + angle); + + draw_rect(fe, rot->cx, rot->cy, rot->cw, rot->ch, bgcolour); + } else + rot = NULL; + + /* + * Now draw each tile. + */ + for (i = 0; i < state->w * state->h; i++) { + int t; + int tx = i % state->w, ty = i / state->w; + + /* + * Figure out what should be displayed at this location. + * Usually it will be state->grid[i], unless we're in the + * middle of animating an actual rotation and this cell is + * within the rotation region, in which case we set -1 + * (always display). + */ + if (oldstate && lastx >= 0 && lasty >= 0 && + tx >= lastx && tx < lastx + state->n && + ty >= lasty && ty < lasty + state->n) + t = -1; + else + t = state->grid[i]; + + if (ds->bgcolour != bgcolour || /* always redraw when flashing */ + ds->grid[i] != t || ds->grid[i] == -1 || t == -1) { + int x = COORD(tx), y = COORD(ty); + + draw_tile(fe, state, x, y, state->grid[i], bgcolour, rot); + ds->grid[i] = t; + } + } + ds->bgcolour = bgcolour; + + /* + * Update the status bar. + */ + { + char statusbuf[256]; + + /* + * Don't show the new status until we're also showing the + * new _state_ - after the game animation is complete. + */ + if (oldstate) + state = oldstate; + + sprintf(statusbuf, "%sMoves: %d", + (state->completed ? "COMPLETED! " : ""), + (state->completed ? state->completed : state->movecount)); + + status_bar(fe, statusbuf); + } +} + +static int game_wants_statusbar(void) +{ + return TRUE; +} + +#ifdef COMBINED +#define thegame twiddle +#endif + +const struct game thegame = { + "Twiddle", "games.twiddle", TRUE, + default_params, + game_fetch_preset, + decode_params, + encode_params, + free_params, + dup_params, + game_configure, + custom_params, + validate_params, + new_game_seed, + validate_seed, + new_game, + dup_game, + free_game, + new_ui, + free_ui, + make_move, + game_size, + game_colours, + game_new_drawstate, + game_free_drawstate, + game_redraw, + game_anim_length, + game_flash_length, + game_wants_statusbar, +}; -- 2.11.0