Files
puzzles/fifteen.c
Simon Tatham 76d50e6905 Re-architecting of the game backend interface. make_move() has been
split into two functions. The first, interpret_move(), takes all the
arguments that make_move() used to get and may have the usual side
effects of modifying the game_ui, but instead of returning a
modified game_state it instead returns a string description of the
move to be made. This string description is then passed to a second
function, execute_move(), together with an input game_state, which
is responsible for actually producing the new state. (solve_game()
also returns a string to be passed to execute_move().)

The point of this is to work towards being able to serialise the
whole of a game midend into a byte stream such as a disk file, which
will eventually support save and load functions in the desktop
puzzles, as well as restoring half-finished games after a quit and
restart in James Harvey's Palm port. Making each game supply a
convert-to-string function for its game_state format would have been
an unreliable way to do this, since those functions would not have
been used in normal play, so they'd only have been tested when you
actually tried to save and load - a recipe for latent bugs if ever I
heard one. This way, you won't even be able to _make_ a move if
execute_move() doesn't work properly, which means that if you can
play a game at all I can have pretty high confidence that
serialising it will work first time.

This is only the groundwork; there will be more checkins to come on
this theme. But the major upheaval should now be done, and as far as
I can tell everything's still working normally.

[originally from svn r6024]
2005-06-27 19:34:54 +00:00

899 lines
22 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 PREFERRED_TILE_SIZE 48
#define TILE_SIZE (ds->tilesize)
#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 interactive)
{
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(midend_data *me, 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->tiles);
sfree(state);
}
static char *solve_game(game_state *state, game_state *currstate,
game_aux_info *aux, char **error)
{
return dupstr("S");
}
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 void game_changed_state(game_ui *ui, game_state *oldstate,
game_state *newstate)
{
}
struct game_drawstate {
int started;
int w, h, bgcolour;
int *tiles;
int tilesize;
};
static char *interpret_move(game_state *state, game_ui *ui, game_drawstate *ds,
int x, int y, int button)
{
int gx, gy, dx, dy;
char buf[80];
button &= ~MOD_MASK;
gx = X(state, state->gap_pos);
gy = Y(state, state->gap_pos);
if (button == CURSOR_RIGHT && gx > 0)
dx = gx - 1, dy = gy;
else if (button == CURSOR_LEFT && gx < state->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 < state->h-1)
dy = gy + 1, dx = gx;
else if (button == LEFT_BUTTON) {
dx = FROMCOORD(x);
dy = FROMCOORD(y);
if (dx < 0 || dx >= state->w || dy < 0 || dy >= state->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 */
sprintf(buf, "M%d,%d", dx, dy);
return dupstr(buf);
}
static game_state *execute_move(game_state *from, char *move)
{
int gx, gy, dx, dy, ux, uy, up, p;
game_state *ret;
if (!strcmp(move, "S")) {
int i;
ret = dup_game(from);
/*
* 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;
}
gx = X(from, from->gap_pos);
gy = Y(from, from->gap_pos);
if (move[0] != 'M' ||
sscanf(move+1, "%d,%d", &dx, &dy) != 2 ||
(dx == gx && dy == gy) || (dx != gx && dy != gy) ||
dx < 0 || dx >= from->w || dy < 0 || dy >= from->h)
return NULL;
/*
* 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.
*/
static void game_size(game_params *params, game_drawstate *ds,
int *x, int *y, int expand)
{
int tsx, tsy, ts;
/*
* Each window dimension equals the tile size times one more
* than the grid dimension (the border is half the width of the
* tiles).
*/
tsx = *x / (params->w + 1);
tsy = *y / (params->h + 1);
ts = min(tsx, tsy);
if (expand)
ds->tilesize = ts;
else
ds->tilesize = min(ts, PREFERRED_TILE_SIZE);
*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);
ds->tilesize = 0; /* haven't decided yet */
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_drawstate *ds, 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[10];
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] = coords[2] - TILE_SIZE;
coords[5] = coords[3] + TILE_SIZE;
coords[8] = COORD(0) - HIGHLIGHT_WIDTH;
coords[9] = COORD(state->h) + HIGHLIGHT_WIDTH - 1;
coords[6] = coords[8] + TILE_SIZE;
coords[7] = coords[9] - TILE_SIZE;
draw_polygon(fe, coords, 5, TRUE, COL_HIGHLIGHT);
draw_polygon(fe, coords, 5, FALSE, COL_HIGHLIGHT);
coords[1] = COORD(0) - HIGHLIGHT_WIDTH;
coords[0] = COORD(0) - HIGHLIGHT_WIDTH;
draw_polygon(fe, coords, 5, TRUE, COL_LOWLIGHT);
draw_polygon(fe, coords, 5, 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, ds, 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, game_ui *ui)
{
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, game_ui *ui)
{
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;
}
static int game_timing_state(game_state *state)
{
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,
game_changed_state,
interpret_move,
execute_move,
game_size,
game_colours,
game_new_drawstate,
game_free_drawstate,
game_redraw,
game_anim_length,
game_flash_length,
game_wants_statusbar,
FALSE, game_timing_state,
0, /* mouse_priorities */
};