* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
struct game_params {
int w, h, n;
int rowsonly;
+ int orientable;
+ int movetarget;
};
struct game_state {
int w, h, n;
+ int orientable;
int *grid;
int completed;
- int movecount;
+ int just_used_solve; /* used to suppress undo animation */
+ int used_solve; /* used to suppress completion flash */
+ int movecount, movetarget;
int lastx, lasty, lastr; /* coordinates of last rotation */
};
ret->w = ret->h = 3;
ret->n = 2;
- ret->rowsonly = FALSE;
+ ret->rowsonly = ret->orientable = FALSE;
+ ret->movetarget = 0;
return ret;
}
char *title;
game_params params;
} presets[] = {
- { "3x3 rows only", { 3, 3, 2, TRUE } },
- { "3x3 normal", { 3, 3, 2, FALSE } },
+ { "3x3 rows only", { 3, 3, 2, TRUE, FALSE } },
+ { "3x3 normal", { 3, 3, 2, FALSE, FALSE } },
+ { "3x3 orientable", { 3, 3, 2, FALSE, TRUE } },
{ "4x4 normal", { 4, 4, 2, FALSE } },
+ { "4x4 orientable", { 4, 4, 2, FALSE, TRUE } },
{ "4x4 radius 3", { 4, 4, 3, FALSE } },
{ "5x5 radius 3", { 5, 5, 3, FALSE } },
{ "6x6 radius 4", { 6, 6, 4, FALSE } },
return TRUE;
}
-static game_params *decode_params(char const *string)
+static void decode_params(game_params *ret, char const *string)
{
- game_params *ret = snew(game_params);
-
ret->w = ret->h = atoi(string);
ret->n = 2;
- ret->rowsonly = FALSE;
+ ret->rowsonly = ret->orientable = FALSE;
+ ret->movetarget = 0;
while (*string && isdigit(*string)) string++;
if (*string == 'x') {
string++;
ret->n = atoi(string);
while (*string && isdigit(*string)) string++;
}
- if (*string == 'r') {
+ while (*string) {
+ if (*string == 'r') {
+ 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++;
- ret->rowsonly = TRUE;
}
-
- return ret;
}
-static char *encode_params(game_params *params)
+static char *encode_params(game_params *params, int full)
{
char buf[256];
- sprintf(buf, "%dx%dn%d%s", params->w, params->h, params->n,
- params->rowsonly ? "r" : "");
+ sprintf(buf, "%dx%dn%d%s%s", params->w, params->h, params->n,
+ params->rowsonly ? "r" : "",
+ params->orientable ? "o" : "");
+ /* Shuffle limit is part of the limited parameters, because we have to
+ * supply the target move count. */
+ if (params->movetarget)
+ sprintf(buf + strlen(buf), "m%d", params->movetarget);
return dupstr(buf);
}
config_item *ret;
char buf[80];
- ret = snewn(4, config_item);
+ ret = snewn(7, config_item);
ret[0].name = "Width";
ret[0].type = C_STRING;
ret[3].sval = NULL;
ret[3].ival = params->rowsonly;
- ret[4].name = NULL;
- ret[4].type = C_END;
+ ret[4].name = "Orientation matters";
+ ret[4].type = C_BOOLEAN;
ret[4].sval = NULL;
- ret[4].ival = 0;
+ ret[4].ival = params->orientable;
+
+ 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;
}
ret->h = atoi(cfg[1].sval);
ret->n = atoi(cfg[2].sval);
ret->rowsonly = cfg[3].ival;
+ ret->orientable = cfg[4].ival;
+ ret->movetarget = atoi(cfg[5].sval);
return ret;
}
* 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)
+static void do_rotate(int *grid, int w, int h, int n, int orientable,
+ int x, int y, int dir)
{
int i, 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];
+ for (k = 0; k < 4; k++) {
+ int v = g[(k+dir) & 3];
+ if (orientable)
+ v ^= ((v+dir) ^ v) & 3; /* alter orientation */
+ grid[p[k]] = v;
+ }
}
}
+
+ /*
+ * Don't forget the orientation on the centre square, if n is
+ * odd.
+ */
+ if (orientable && (n & 1)) {
+ int v = grid[n/2*(w+1)];
+ v ^= ((v+dir) ^ v) & 3; /* alter orientation */
+ grid[n/2*(w+1)] = v;
+ }
}
-static int grid_complete(int *grid, int wh)
+static int grid_complete(int *grid, int wh, int orientable)
{
int ok = TRUE;
int i;
for (i = 1; i < wh; i++)
if (grid[i] < grid[i-1])
ok = FALSE;
+ if (orientable) {
+ for (i = 0; i < wh; i++)
+ if (grid[i] & 3)
+ ok = FALSE;
+ }
return ok;
}
-static char *new_game_seed(game_params *params, random_state *rs)
+static char *new_game_desc(game_params *params, random_state *rs,
+ game_aux_info **aux)
{
int *grid;
int w = params->w, h = params->h, n = params->n, wh = w*h;
*/
grid = snewn(wh, int);
for (i = 0; i < wh; i++)
- grid[i] = (params->rowsonly ? i/w : i) + 1;
+ grid[i] = ((params->rowsonly ? i/w : i) + 1) * 4;
/*
* Shuffle it. This game is complex enough that I don't feel up
* 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--;
- }
+ total_moves = params->movetarget;
+ if (!total_moves)
+ total_moves = w*h*n*n*2 + random_upto(rs, 2);
+
+ do {
+ int oldx = -1, oldy = -1, oldr = -1;
+
+ 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
- * simple sequence of comma-separated integers.
+ * Now construct the game description, by describing the grid
+ * as a simple sequence of integers. They're comma-separated,
+ * unless the puzzle is orientable in which case they're
+ * separated by orientation letters `u', `d', `l' and `r'.
*/
ret = NULL;
retlen = 0;
char buf[80];
int k;
- k = sprintf(buf, "%d,", grid[i]);
+ k = sprintf(buf, "%d%c", grid[i] / 4,
+ params->orientable ? "uldr"[grid[i] & 3] : ',');
ret = sresize(ret, retlen + k + 1, char);
strcpy(ret + retlen, buf);
retlen += k;
}
- ret[retlen-1] = '\0'; /* delete last comma */
+ if (!params->orientable)
+ ret[retlen-1] = '\0'; /* delete last comma */
sfree(grid);
return ret;
}
-static char *validate_seed(game_params *params, char *seed)
+static void game_free_aux_info(game_aux_info *aux)
+{
+ assert(!"Shouldn't happen");
+}
+
+static char *validate_desc(game_params *params, char *desc)
{
char *p, *err;
int w = params->w, h = params->h, wh = w*h;
int i;
- p = seed;
+ p = desc;
err = NULL;
for (i = 0; i < wh; i++) {
- if (*p < '0' || *p > '9') {
+ 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) {
+ if (!params->orientable && i < wh-1) {
+ if (*p != ',')
+ return "Expected comma after number";
+ } else if (params->orientable && i < wh) {
+ if (*p != 'l' && *p != 'r' && *p != 'u' && *p != 'd')
+ return "Expected orientation letter after number";
+ } else if (i == wh-1 && *p) {
return "Excess junk at end of string";
}
return NULL;
}
-static game_state *new_game(game_params *params, char *seed)
+static game_state *new_game(midend_data *me, game_params *params, char *desc)
{
game_state *state = snew(game_state);
int w = params->w, h = params->h, n = params->n, wh = w*h;
state->w = w;
state->h = h;
state->n = n;
+ state->orientable = params->orientable;
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);
- p = seed;
+ p = desc;
for (i = 0; i < wh; i++) {
- state->grid[i] = atoi(p);
+ state->grid[i] = 4 * atoi(p);
while (*p >= '0' && *p <= '9')
p++;
-
- if (*p) p++; /* eat comma */
+ if (*p) {
+ if (params->orientable) {
+ switch (*p) {
+ case 'l': state->grid[i] |= 1; break;
+ case 'd': state->grid[i] |= 2; break;
+ case 'r': state->grid[i] |= 3; break;
+ }
+ }
+ p++;
+ }
}
return state;
ret->w = state->w;
ret->h = state->h;
ret->n = state->n;
+ 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;
+ ret->used_solve = state->used_solve;
+ ret->just_used_solve = state->just_used_solve;
ret->grid = snewn(ret->w * ret->h, int);
memcpy(ret->grid, state->grid, ret->w * ret->h * sizeof(int));
sfree(state);
}
+static int compare_int(const void *av, const void *bv)
+{
+ const int *a = (const int *)av;
+ const int *b = (const int *)bv;
+ if (*a < *b)
+ return -1;
+ else if (*a > *b)
+ return +1;
+ else
+ return 0;
+}
+
+static game_state *solve_game(game_state *state, game_aux_info *aux,
+ char **error)
+{
+ game_state *ret = dup_game(state);
+ int i;
+
+ /*
+ * Simply replace the grid with a solved one. For this game,
+ * this isn't a useful operation for actually telling the user
+ * what they should have done, but it is useful for
+ * conveniently being able to get hold of a clean state from
+ * which to practise manoeuvres.
+ */
+ qsort(ret->grid, ret->w*ret->h, sizeof(int), compare_int);
+ for (i = 0; i < ret->w*ret->h; i++)
+ ret->grid[i] &= ~3;
+ ret->used_solve = ret->just_used_solve = TRUE;
+ ret->completed = ret->movecount = 1;
+
+ return ret;
+}
+
+static char *game_text_format(game_state *state)
+{
+ char *ret, *p, buf[80];
+ int i, x, y, col, o, maxlen;
+
+ /*
+ * First work out how many characters we need to display each
+ * number. We're pretty flexible on grid contents here, so we
+ * have to scan the entire grid.
+ */
+ col = 0;
+ for (i = 0; i < state->w * state->h; i++) {
+ x = sprintf(buf, "%d", state->grid[i] / 4);
+ if (col < x) col = x;
+ }
+ o = (state->orientable ? 1 : 0);
+
+ /*
+ * Now we know the exact total size of the grid we're going to
+ * produce: it's got h rows, each containing w lots of col+o,
+ * w-1 spaces and a trailing newline.
+ */
+ maxlen = state->h * state->w * (col+o+1);
+
+ ret = snewn(maxlen+1, char);
+ p = ret;
+
+ for (y = 0; y < state->h; y++) {
+ for (x = 0; x < state->w; x++) {
+ int v = state->grid[state->w*y+x];
+ sprintf(buf, "%*d", col, v/4);
+ memcpy(p, buf, col);
+ p += col;
+ if (o)
+ *p++ = "^<v>"[v & 3];
+ if (x+1 == state->w)
+ *p++ = '\n';
+ else
+ *p++ = ' ';
+ }
+ }
+
+ assert(p - ret == maxlen);
+ *p = '\0';
+ return ret;
+}
+
static game_ui *new_ui(game_state *state)
{
return NULL;
game_state *ret;
int dir;
+ button = button & (~MOD_MASK | MOD_NUM_KEYPAD);
+
if (button == LEFT_BUTTON || button == RIGHT_BUTTON) {
/*
* Determine the coordinates of the click. We offset by n-1
y -= (n-1) * TILE_SIZE / 2;
x = FROMCOORD(x);
y = FROMCOORD(y);
- if (x < 0 || x > w-n || y < 0 || y > w-n)
+ dir = (button == LEFT_BUTTON ? 1 : -1);
+ if (x < 0 || x > w-n || y < 0 || y > h-n)
return NULL;
+ } else if (button == 'a' || button == 'A' || button==MOD_NUM_KEYPAD+'7') {
+ x = y = 0;
+ dir = (button == 'A' ? -1 : +1);
+ } else if (button == 'b' || button == 'B' || button==MOD_NUM_KEYPAD+'9') {
+ x = w-n;
+ y = 0;
+ dir = (button == 'B' ? -1 : +1);
+ } else if (button == 'c' || button == 'C' || button==MOD_NUM_KEYPAD+'1') {
+ x = 0;
+ y = h-n;
+ dir = (button == 'C' ? -1 : +1);
+ } else if (button == 'd' || button == 'D' || button==MOD_NUM_KEYPAD+'3') {
+ x = w-n;
+ y = h-n;
+ dir = (button == 'D' ? -1 : +1);
+ } else if (button==MOD_NUM_KEYPAD+'8' && (w-n) % 2 == 0) {
+ x = (w-n) / 2;
+ y = 0;
+ dir = +1;
+ } else if (button==MOD_NUM_KEYPAD+'2' && (w-n) % 2 == 0) {
+ x = (w-n) / 2;
+ y = h-n;
+ dir = +1;
+ } else if (button==MOD_NUM_KEYPAD+'4' && (h-n) % 2 == 0) {
+ x = 0;
+ y = (h-n) / 2;
+ dir = +1;
+ } else if (button==MOD_NUM_KEYPAD+'6' && (h-n) % 2 == 0) {
+ x = w-n;
+ y = (h-n) / 2;
+ dir = +1;
+ } else if (button==MOD_NUM_KEYPAD+'5' && (w-n) % 2 == 0 && (h-n) % 2 == 0){
+ x = (w-n) / 2;
+ y = (h-n) / 2;
+ dir = +1;
+ } else {
+ return NULL; /* no move to be made */
+ }
- /*
- * 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;
+ /*
+ * This is a valid move. Make it.
+ */
+ ret = dup_game(from);
+ ret->just_used_solve = FALSE; /* zero this in a hurry */
+ ret->movecount++;
+ do_rotate(ret->grid, w, h, n, ret->orientable, 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;
+ /*
+ * 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->orientable))
+ ret->completed = ret->movecount;
+ return ret;
}
/* ----------------------------------------------------------------------
int coords[8];
char str[40];
+ /*
+ * If we've been passed a rotation region but we're drawing a
+ * tile which is outside it, we must draw it normally. This can
+ * occur if we're cleaning up after a completion flash while a
+ * new move is also being made.
+ */
+ if (rot && (x < rot->cx || y < rot->cy ||
+ x >= rot->cx+rot->cw || y >= rot->cy+rot->ch))
+ rot = NULL;
+
if (rot)
clip(fe, rot->cx, rot->cy, rot->cw, rot->ch);
draw_polygon(fe, coords, 3, TRUE, rot ? rot->tc : COL_HIGHLIGHT);
draw_polygon(fe, coords, 3, FALSE, rot ? rot->tc : COL_HIGHLIGHT);
+ /*
+ * Now the main blank area in the centre of the tile.
+ */
if (rot) {
coords[0] = x + HIGHLIGHT_WIDTH;
coords[1] = y + HIGHLIGHT_WIDTH;
flash_colour);
}
+ /*
+ * Next, the triangles for orientation.
+ */
+ if (state->orientable) {
+ int xdx, xdy, ydx, ydy;
+ int cx, cy, displ, displ2;
+ switch (tile & 3) {
+ case 0:
+ xdx = 1, xdy = 0;
+ ydx = 0, ydy = 1;
+ break;
+ case 1:
+ xdx = 0, xdy = -1;
+ ydx = 1, ydy = 0;
+ break;
+ case 2:
+ xdx = -1, xdy = 0;
+ ydx = 0, ydy = -1;
+ break;
+ default /* case 3 */:
+ xdx = 0, xdy = 1;
+ ydx = -1, ydy = 0;
+ break;
+ }
+
+ cx = x + TILE_SIZE / 2;
+ cy = y + TILE_SIZE / 2;
+ displ = TILE_SIZE / 2 - HIGHLIGHT_WIDTH - 2;
+ displ2 = TILE_SIZE / 3 - HIGHLIGHT_WIDTH;
+
+ coords[0] = cx - displ * xdx + displ2 * ydx;
+ coords[1] = cy - displ * xdy + displ2 * ydy;
+ rotate(coords+0, rot);
+ coords[2] = cx + displ * xdx + displ2 * ydx;
+ coords[3] = cy + displ * xdy + displ2 * ydy;
+ rotate(coords+2, rot);
+ coords[4] = cx - displ * ydx;
+ coords[5] = cy - displ * ydy;
+ rotate(coords+4, rot);
+ draw_polygon(fe, coords, 3, TRUE, COL_LOWLIGHT_GENTLE);
+ draw_polygon(fe, coords, 3, FALSE, COL_LOWLIGHT_GENTLE);
+ }
+
coords[0] = x + TILE_SIZE/2;
coords[1] = y + TILE_SIZE/2;
rotate(coords+0, rot);
- sprintf(str, "%d", tile);
+ sprintf(str, "%d", tile / 4);
draw_text(fe, coords[0], coords[1],
FONT_VARIABLE, TILE_SIZE/3, ALIGN_VCENTRE | ALIGN_HCENTRE,
COL_TEXT, str);
}
static float game_anim_length(game_state *oldstate, game_state *newstate,
- int dir)
+ int dir, game_ui *ui)
{
- return ANIM_PER_RADIUS_UNIT * sqrt(newstate->n-1);
+ if ((dir > 0 && newstate->just_used_solve) ||
+ (dir < 0 && oldstate->just_used_solve))
+ return 0.0F;
+ else
+ return ANIM_PER_RADIUS_UNIT * sqrt(newstate->n-1);
}
static float game_flash_length(game_state *oldstate, game_state *newstate,
- int dir)
+ int dir, game_ui *ui)
{
- if (!oldstate->completed && newstate->completed)
+ if (!oldstate->completed && newstate->completed &&
+ !oldstate->used_solve && !newstate->used_solve)
return 2 * FLASH_FRAME;
else
return 0.0F;
*/
if (oldstate) {
float angle;
- float anim_max = game_anim_length(oldstate, state, dir);
+ float anim_max = game_anim_length(oldstate, state, dir, ui);
if (dir > 0) {
lastx = state->lastx;
if (oldstate)
state = oldstate;
- sprintf(statusbuf, "%sMoves: %d",
- (state->completed ? "COMPLETED! " : ""),
- (state->completed ? state->completed : state->movecount));
+ if (state->used_solve)
+ sprintf(statusbuf, "Moves since auto-solve: %d",
+ state->movecount - state->completed);
+ 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);
}
#endif
const struct game thegame = {
- "Twiddle", "games.twiddle", TRUE,
+ "Twiddle", "games.twiddle",
default_params,
game_fetch_preset,
decode_params,
encode_params,
free_params,
dup_params,
- game_configure,
- custom_params,
+ TRUE, game_configure, custom_params,
validate_params,
- new_game_seed,
- validate_seed,
+ new_game_desc,
+ game_free_aux_info,
+ validate_desc,
new_game,
dup_game,
free_game,
+ TRUE, solve_game,
+ TRUE, game_text_format,
new_ui,
free_ui,
make_move,