Files
puzzles/fifteen.c
Simon Tatham 2534ec5d69 The game IDs for Net (and Netslide) have always been random seeds
rather than literal grid descriptions, which has always faintly
annoyed me because it makes it impossible to type in a grid from
another source. However, Gareth pointed out that short random-seed
game descriptions are useful, because you can read one out to
someone else without having to master the technology of cross-
machine cut and paste, or you can have two people enter the same
random seed simultaneously in order to race against each other to
complete the same puzzle. So both types of game ID seem to have
their uses.

Therefore, here's a reorganisation of the whole game ID concept.
There are now two types of game ID: one has a parameter string then
a hash then a piece of arbitrary random seed text, and the other has
a parameter string then a colon then a literal game description. For
most games, the latter is identical to the game IDs that were
previously valid; for Net and Netslide, old game IDs must be
translated into new ones by turning the colon into a hash, and
there's a new descriptive game ID format.

Random seed IDs are not guaranteed to be portable between software
versions (this is a major reason why I added version reporting
yesterday). Descriptive game IDs have a longer lifespan.

As an added bonus, I've removed the sections of documentation
dealing with game parameter encodings not shown in the game ID
(Rectangles expansion factor, Solo symmetry and difficulty settings
etc), because _all_ parameters must be specified in a random seed ID
and therefore users can easily find out the appropriate parameter
string for any settings they have configured.

[originally from svn r5788]
2005-05-16 18:57:09 +00:00

837 lines
20 KiB
C

/*
* fifteen.c: standard 15-puzzle.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <ctype.h>
#include <math.h>
#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 ANIM_TIME 0.13F
#define FLASH_FRAME 0.13F
#define X(state, i) ( (i) % (state)->w )
#define Y(state, i) ( (i) / (state)->w )
#define C(state, x, y) ( (y) * (state)->w + (x) )
enum {
COL_BACKGROUND,
COL_TEXT,
COL_HIGHLIGHT,
COL_LOWLIGHT,
NCOLOURS
};
struct game_params {
int w, h;
};
struct game_state {
int w, h, n;
int *tiles;
int gap_pos;
int completed;
int just_used_solve; /* used to suppress undo animation */
int used_solve; /* used to suppress completion flash */
int movecount;
};
static game_params *default_params(void)
{
game_params *ret = snew(game_params);
ret->w = ret->h = 4;
return ret;
}
static int game_fetch_preset(int i, char **name, game_params **params)
{
return FALSE;
}
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 void decode_params(game_params *ret, char const *string)
{
ret->w = ret->h = atoi(string);
while (*string && isdigit(*string)) string++;
if (*string == 'x') {
string++;
ret->h = atoi(string);
}
}
static char *encode_params(game_params *params, int full)
{
char data[256];
sprintf(data, "%dx%d", params->w, params->h);
return dupstr(data);
}
static config_item *game_configure(game_params *params)
{
config_item *ret;
char buf[80];
ret = snewn(3, 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 = NULL;
ret[2].type = C_END;
ret[2].sval = NULL;
ret[2].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);
return ret;
}
static char *validate_params(game_params *params)
{
if (params->w < 2 && params->h < 2)
return "Width and height must both be at least two";
return NULL;
}
static int perm_parity(int *perm, int n)
{
int i, j, ret;
ret = 0;
for (i = 0; i < n-1; i++)
for (j = i+1; j < n; j++)
if (perm[i] > perm[j])
ret = !ret;
return ret;
}
static char *new_game_desc(game_params *params, random_state *rs,
game_aux_info **aux)
{
int gap, n, i, x;
int x1, x2, p1, p2, parity;
int *tiles, *used;
char *ret;
int retlen;
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;
}
gap = random_upto(rs, n);
tiles[gap] = 0;
used[0] = TRUE;
/*
* Place everything else except the last two tiles.
*/
for (x = 0, i = n-1; i > 2; i--) {
int k = random_upto(rs, i);
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;
}
/*
* 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;
/*
* Determine the required parity of the overall permutation.
* This is the XOR of:
*
* - The chessboard parity ((x^y)&1) of the gap square. The
* bottom right counts as even.
*
* - The parity of n. (The target permutation is 1,...,n-1,0
* rather than 0,...,n-1; this is a cyclic permutation of
* the starting point and hence is odd iff n is even.)
*/
parity = ((X(params, gap) - (params->w-1)) ^
(Y(params, gap) - (params->h-1)) ^
(n+1)) & 1;
/*
* Try the last two tiles one way round. If that fails, swap
* them.
*/
tiles[x1] = p1;
tiles[x2] = p2;
if (perm_parity(tiles, n) != parity) {
tiles[x1] = p2;
tiles[x2] = p1;
assert(perm_parity(tiles, n) == parity);
}
/*
* Now construct the game description, by describing the tile
* array as a simple sequence of comma-separated integers.
*/
ret = NULL;
retlen = 0;
for (i = 0; i < n; i++) {
char buf[80];
int k;
k = sprintf(buf, "%d,", tiles[i]);
ret = sresize(ret, retlen + k + 1, char);
strcpy(ret + retlen, buf);
retlen += k;
}
ret[retlen-1] = '\0'; /* delete last comma */
sfree(tiles);
sfree(used);
return ret;
}
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 i, area;
int *used;
area = params->w * params->h;
p = desc;
err = NULL;
used = snewn(area, int);
for (i = 0; i < area; i++)
used[i] = FALSE;
for (i = 0; i < area; i++) {
char *q = p;
int n;
if (*p < '0' || *p > '9') {
err = "Not enough numbers in string";
goto leave;
}
while (*p >= '0' && *p <= '9')
p++;
if (i < area-1 && *p != ',') {
err = "Expected comma after number";
goto leave;
}
else if (i == area-1 && *p) {
err = "Excess junk at end of string";
goto leave;
}
n = atoi(q);
if (n < 0 || n >= area) {
err = "Number out of range";
goto leave;
}
if (used[n]) {
err = "Number used twice";
goto leave;
}
used[n] = TRUE;
if (*p) p++; /* eat comma */
}
leave:
sfree(used);
return err;
}
static game_state *new_game(game_params *params, char *desc)
{
game_state *state = snew(game_state);
int i;
char *p;
state->w = params->w;
state->h = params->h;
state->n = params->w * params->h;
state->tiles = snewn(state->n, int);
state->gap_pos = 0;
p = desc;
i = 0;
for (i = 0; i < state->n; i++) {
assert(*p);
state->tiles[i] = atoi(p);
if (state->tiles[i] == 0)
state->gap_pos = i;
while (*p && *p != ',')
p++;
if (*p) p++; /* eat comma */
}
assert(!*p);
assert(state->tiles[state->gap_pos] == 0);
state->completed = state->movecount = 0;
state->used_solve = state->just_used_solve = FALSE;
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->tiles = snewn(state->w * state->h, int);
memcpy(ret->tiles, state->tiles, state->w * state->h * sizeof(int));
ret->gap_pos = state->gap_pos;
ret->completed = state->completed;
ret->movecount = state->movecount;
ret->used_solve = state->used_solve;
ret->just_used_solve = state->just_used_solve;
return ret;
}
static void free_game(game_state *state)
{
sfree(state);
}
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.
*/
for (i = 0; i < ret->n; i++)
ret->tiles[i] = (i+1) % ret->n;
ret->gap_pos = ret->n-1;
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 x, y, col, maxlen;
/*
* First work out how many characters we need to display each
* number.
*/
col = sprintf(buf, "%d", state->n-1);
/*
* 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, w-1
* spaces and a trailing newline.
*/
maxlen = state->h * state->w * (col+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->tiles[state->w*y+x];
if (v == 0)
sprintf(buf, "%*s", col, "");
else
sprintf(buf, "%*d", col, v);
memcpy(p, buf, col);
p += col;
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;
}
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 gx, gy, dx, dy, ux, uy, up, p;
game_state *ret;
gx = X(from, from->gap_pos);
gy = Y(from, from->gap_pos);
if (button == CURSOR_RIGHT && gx > 0)
dx = gx - 1, dy = gy;
else if (button == CURSOR_LEFT && gx < from->w-1)
dx = gx + 1, dy = gy;
else if (button == CURSOR_DOWN && gy > 0)
dy = gy - 1, dx = gx;
else if (button == CURSOR_UP && gy < from->h-1)
dy = gy + 1, dx = gx;
else if (button == LEFT_BUTTON) {
dx = FROMCOORD(x);
dy = FROMCOORD(y);
if (dx < 0 || dx >= from->w || dy < 0 || dy >= from->h)
return NULL; /* out of bounds */
/*
* Any click location should be equal to the gap location
* in _precisely_ one coordinate.
*/
if ((dx == gx && dy == gy) || (dx != gx && dy != gy))
return NULL;
} else
return NULL; /* no move */
/*
* Find the unit displacement from the original gap
* position towards this one.
*/
ux = (dx < gx ? -1 : dx > gx ? +1 : 0);
uy = (dy < gy ? -1 : dy > gy ? +1 : 0);
up = C(from, ux, uy);
ret = dup_game(from);
ret->just_used_solve = FALSE; /* zero this in a hurry */
ret->gap_pos = C(from, dx, dy);
assert(ret->gap_pos >= 0 && ret->gap_pos < ret->n);
ret->tiles[ret->gap_pos] = 0;
for (p = from->gap_pos; p != ret->gap_pos; p += up) {
assert(p >= 0 && p < from->n);
ret->tiles[p] = from->tiles[p + up];
ret->movecount++;
}
/*
* See if the game has been completed.
*/
if (!ret->completed) {
ret->completed = ret->movecount;
for (p = 0; p < ret->n; p++)
if (ret->tiles[p] != (p < ret->n-1 ? p+1 : 0))
ret->completed = 0;
}
return ret;
}
/* ----------------------------------------------------------------------
* Drawing routines.
*/
struct game_drawstate {
int started;
int w, h, bgcolour;
int *tiles;
};
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_LOWLIGHT * 3 + i] = ret[COL_BACKGROUND * 3 + i] * 0.8F;
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->tiles = snewn(ds->w*ds->h, int);
for (i = 0; i < ds->w*ds->h; i++)
ds->tiles[i] = -1;
return ds;
}
static void game_free_drawstate(game_drawstate *ds)
{
sfree(ds->tiles);
sfree(ds);
}
static void draw_tile(frontend *fe, game_state *state, int x, int y,
int tile, int flash_colour)
{
if (tile == 0) {
draw_rect(fe, x, y, TILE_SIZE, TILE_SIZE,
flash_colour);
} else {
int coords[6];
char str[40];
coords[0] = x + TILE_SIZE - 1;
coords[1] = y + TILE_SIZE - 1;
coords[2] = x + TILE_SIZE - 1;
coords[3] = y;
coords[4] = x;
coords[5] = y + TILE_SIZE - 1;
draw_polygon(fe, coords, 3, TRUE, COL_LOWLIGHT);
draw_polygon(fe, coords, 3, FALSE, COL_LOWLIGHT);
coords[0] = x;
coords[1] = y;
draw_polygon(fe, coords, 3, TRUE, COL_HIGHLIGHT);
draw_polygon(fe, coords, 3, FALSE, COL_HIGHLIGHT);
draw_rect(fe, x + HIGHLIGHT_WIDTH, y + HIGHLIGHT_WIDTH,
TILE_SIZE - 2*HIGHLIGHT_WIDTH, TILE_SIZE - 2*HIGHLIGHT_WIDTH,
flash_colour);
sprintf(str, "%d", tile);
draw_text(fe, x + TILE_SIZE/2, y + TILE_SIZE/2,
FONT_VARIABLE, TILE_SIZE/3, ALIGN_VCENTRE | ALIGN_HCENTRE,
COL_TEXT, str);
}
draw_update(fe, x, y, TILE_SIZE, TILE_SIZE);
}
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, pass, bgcolour;
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;
}
/*
* Now draw each tile. We do this in two passes to make
* animation easy.
*/
for (pass = 0; pass < 2; pass++) {
for (i = 0; i < state->n; i++) {
int t, t0;
/*
* Figure out what should be displayed at this
* location. It's either a simple tile, or it's a
* transition between two tiles (in which case we say
* -1 because it must always be drawn).
*/
if (oldstate && oldstate->tiles[i] != state->tiles[i])
t = -1;
else
t = state->tiles[i];
t0 = t;
if (ds->bgcolour != bgcolour || /* always redraw when flashing */
ds->tiles[i] != t || ds->tiles[i] == -1 || t == -1) {
int x, y;
/*
* Figure out what to _actually_ draw, and where to
* draw it.
*/
if (t == -1) {
int x0, y0, x1, y1;
int j;
/*
* On the first pass, just blank the tile.
*/
if (pass == 0) {
x = COORD(X(state, i));
y = COORD(Y(state, i));
t = 0;
} else {
float c;
t = state->tiles[i];
/*
* Don't bother moving the gap; just don't
* draw it.
*/
if (t == 0)
continue;
/*
* Find the coordinates of this tile in the old and
* new states.
*/
x1 = COORD(X(state, i));
y1 = COORD(Y(state, i));
for (j = 0; j < oldstate->n; j++)
if (oldstate->tiles[j] == state->tiles[i])
break;
assert(j < oldstate->n);
x0 = COORD(X(state, j));
y0 = COORD(Y(state, j));
c = (animtime / ANIM_TIME);
if (c < 0.0F) c = 0.0F;
if (c > 1.0F) c = 1.0F;
x = x0 + (int)(c * (x1 - x0));
y = y0 + (int)(c * (y1 - y0));
}
} else {
if (pass == 0)
continue;
x = COORD(X(state, i));
y = COORD(Y(state, i));
}
draw_tile(fe, state, x, y, t, bgcolour);
}
ds->tiles[i] = t0;
}
}
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;
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));
status_bar(fe, statusbuf);
}
}
static float game_anim_length(game_state *oldstate,
game_state *newstate, int dir)
{
if ((dir > 0 && newstate->just_used_solve) ||
(dir < 0 && oldstate->just_used_solve))
return 0.0F;
else
return ANIM_TIME;
}
static float game_flash_length(game_state *oldstate,
game_state *newstate, int dir)
{
if (!oldstate->completed && newstate->completed &&
!oldstate->used_solve && !newstate->used_solve)
return 2 * FLASH_FRAME;
else
return 0.0F;
}
static int game_wants_statusbar(void)
{
return TRUE;
}
#ifdef COMBINED
#define thegame fifteen
#endif
const struct game thegame = {
"Fifteen", "games.fifteen",
default_params,
game_fetch_preset,
decode_params,
encode_params,
free_params,
dup_params,
TRUE, game_configure, custom_params,
validate_params,
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,
game_size,
game_colours,
game_new_drawstate,
game_free_drawstate,
game_redraw,
game_anim_length,
game_flash_length,
game_wants_statusbar,
};