From fa7ef572c782c9394f60202d950d3380dfdce5c3 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 29 Apr 2004 18:10:22 +0000 Subject: [PATCH] Implemented text and clipping primitives in the frontend, and added two new simple games `fifteen' and `sixteen'. [originally from svn r4173] --- .cvsignore | 2 +- Recipe | 6 +- cube.c | 26 +-- fifteen.c | 560 +++++++++++++++++++++++++++++++++++++++++++++++++ gtk.c | 89 ++++++++ misc.c | 25 +++ puzzles.h | 19 ++ sixteen.c | 604 +++++++++++++++++++++++++++++++++++++++++++++++++++++ windows.c | 95 +++++++++ 9 files changed, 1401 insertions(+), 25 deletions(-) create mode 100644 fifteen.c create mode 100644 misc.c create mode 100644 sixteen.c diff --git a/.cvsignore b/.cvsignore index c09cfea..faa8661 100644 --- a/.cvsignore +++ b/.cvsignore @@ -1,5 +1,5 @@ Makefile* -net cube nullgame +net cube fifteen sixteen nullgame *.exe *.obj *.o *.map *.rsp *notes diff --git a/Recipe b/Recipe index 52b6a76..19ca3a5 100644 --- a/Recipe +++ b/Recipe @@ -13,14 +13,18 @@ !makefile cygwin Makefile.cyg WINDOWS = windows user32.lib gdi32.lib -COMMON = midend malloc +COMMON = midend misc malloc NET = net random tree234 net : [X] gtk COMMON NET cube : [X] gtk COMMON cube +fifteen : [X] gtk COMMON fifteen +sixteen : [X] gtk COMMON sixteen net : [G] WINDOWS COMMON NET cube : [G] WINDOWS COMMON cube +fifteen : [G] WINDOWS COMMON fifteen +sixteen : [G] WINDOWS COMMON sixteen # The `nullgame' source file is a largely blank one, which contains # all the correct function definitions to compile and link, but diff --git a/cube.c b/cube.c index 1bc08f6..6fef4d1 100644 --- a/cube.c +++ b/cube.c @@ -511,15 +511,7 @@ char *new_game_seed(game_params *params) for (i = 0; i < data.nclasses; i++) { for (j = 0; j < facesperclass; j++) { - unsigned long divisor = RAND_MAX / data.nsquares[i]; - unsigned long max = divisor * data.nsquares[i]; - unsigned long n; - - do { - n = rand(); - } while (n >= max); - - n /= divisor; + int n = rand_upto(data.nsquares[i]); assert(!flags[data.gridptrs[i][n]]); flags[data.gridptrs[i][n]] = TRUE; @@ -529,7 +521,7 @@ char *new_game_seed(game_params *params) * better data structure for this, but for such small * numbers it hardly seems worth the effort. */ - while ((int)n < data.nsquares[i]-1) { + while (n < data.nsquares[i]-1) { data.gridptrs[i][n] = data.gridptrs[i][n+1]; n++; } @@ -567,19 +559,7 @@ char *new_game_seed(game_params *params) /* * Choose a non-blue square for the polyhedron. */ - { - unsigned long divisor = RAND_MAX / m; - unsigned long max = divisor * m; - unsigned long n; - - do { - n = rand(); - } while (n >= max); - - n /= divisor; - - sprintf(p, ":%d", data.gridptrs[0][n]); - } + sprintf(p, ":%d", rand_upto(m)); sfree(data.gridptrs[0]); sfree(flags); diff --git a/fifteen.c b/fifteen.c new file mode 100644 index 0000000..9357d65 --- /dev/null +++ b/fifteen.c @@ -0,0 +1,560 @@ +/* + * fifteen.c: standard 15-puzzle. + */ + +#include +#include +#include +#include +#include + +#include "puzzles.h" + +const char *const game_name = "Fifteen"; + +#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.1F +#define FLASH_FRAME 0.1F + +#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; +}; + +game_params *default_params(void) +{ + game_params *ret = snew(game_params); + + ret->w = ret->h = 4; + + return ret; +} + +int game_fetch_preset(int i, char **name, game_params **params) +{ + return FALSE; +} + +void free_params(game_params *params) +{ + sfree(params); +} + +game_params *dup_params(game_params *params) +{ + game_params *ret = snew(game_params); + *ret = *params; /* structure copy */ + return ret; +} + +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; +} + +char *new_game_seed(game_params *params) +{ + 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 = rand_upto(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 = rand_upto(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, and therefore also the top left, count 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) ^ Y(params, gap) ^ (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 seed, 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; +} + +game_state *new_game(game_params *params, char *seed) +{ + 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 = seed; + 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 = FALSE; + + return state; +} + +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; + + return ret; +} + +void free_game(game_state *state) +{ + sfree(state); +} + +game_state *make_move(game_state *from, 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->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]; + } + + /* + * See if the game has been completed. + */ + if (!ret->completed) { + ret->completed = TRUE; + for (p = 0; p < ret->n; p++) + if (ret->tiles[p] != (p < ret->n-1 ? p+1 : 0)) + ret->completed = FALSE; + } + + return ret; +} + +/* ---------------------------------------------------------------------- + * Drawing routines. + */ + +struct game_drawstate { + int started; + int w, h, bgcolour; + int *tiles; +}; + +void game_size(game_params *params, int *x, int *y) +{ + *x = TILE_SIZE * params->w + 2 * BORDER; + *y = TILE_SIZE * params->h + 2 * BORDER; +} + +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; +} + +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; +} + +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); +} + +void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, + game_state *state, 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; +} + +float game_anim_length(game_state *oldstate, game_state *newstate) +{ + return ANIM_TIME; +} + +float game_flash_length(game_state *oldstate, game_state *newstate) +{ + if (!oldstate->completed && newstate->completed) + return 2 * FLASH_FRAME; + else + return 0.0F; +} diff --git a/gtk.c b/gtk.c index 78eb8e8..778868a 100644 --- a/gtk.c +++ b/gtk.c @@ -34,6 +34,12 @@ void fatal(char *fmt, ...) * GTK front end to puzzles. */ +struct font { + GdkFont *font; + int type; + int size; +}; + /* * This structure holds all the data relevant to a single window. * In principle this would allow us to open multiple independent @@ -53,6 +59,8 @@ struct frontend { GdkGC *gc; int bbox_l, bbox_r, bbox_u, bbox_d; int timer_active, timer_id; + struct font *fonts; + int nfonts, fontsize; }; void frontend_default_colour(frontend *fe, float *output) @@ -72,6 +80,85 @@ void start_draw(frontend *fe) fe->bbox_d = 0; } +void clip(frontend *fe, int x, int y, int w, int h) +{ + GdkRectangle rect; + + rect.x = x; + rect.y = y; + rect.width = w; + rect.height = h; + + gdk_gc_set_clip_rectangle(fe->gc, &rect); +} + +void unclip(frontend *fe) +{ + GdkRectangle rect; + + rect.x = 0; + rect.y = 0; + rect.width = fe->w; + rect.height = fe->h; + + gdk_gc_set_clip_rectangle(fe->gc, &rect); +} + +void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize, + int align, int colour, char *text) +{ + int i; + + /* + * Find or create the font. + */ + for (i = 0; i < fe->nfonts; i++) + if (fe->fonts[i].type == fonttype && fe->fonts[i].size == fontsize) + break; + + if (i == fe->nfonts) { + if (fe->fontsize <= fe->nfonts) { + fe->fontsize = fe->nfonts + 10; + fe->fonts = sresize(fe->fonts, fe->fontsize, struct font); + } + + fe->nfonts++; + + fe->fonts[i].type = fonttype; + fe->fonts[i].size = fontsize; + + /* + * FIXME: Really I should make at least _some_ effort to + * pick the correct font. + */ + fe->fonts[i].font = gdk_font_load("variable"); + } + + /* + * Find string dimensions and process alignment. + */ + { + int lb, rb, wid, asc, desc; + + gdk_string_extents(fe->fonts[i].font, text, + &lb, &rb, &wid, &asc, &desc); + if (align & ALIGN_VCENTRE) + y += asc - (asc+desc)/2; + + if (align & ALIGN_HCENTRE) + x -= wid / 2; + else if (align & ALIGN_HRIGHT) + x -= wid; + + } + + /* + * Set colour and actually draw text. + */ + gdk_gc_set_foreground(fe->gc, &fe->colours[colour]); + gdk_draw_string(fe->pixmap, fe->fonts[i].font, fe->gc, x, y, text); +} + void draw_rect(frontend *fe, int x, int y, int w, int h, int colour) { gdk_gc_set_foreground(fe->gc, &fe->colours[colour]); @@ -405,6 +492,8 @@ static frontend *new_window(void) gtk_box_pack_end(vbox, fe->area, FALSE, FALSE, 0); fe->pixmap = NULL; + fe->fonts = NULL; + fe->nfonts = fe->fontsize = 0; fe->timer_active = FALSE; diff --git a/misc.c b/misc.c new file mode 100644 index 0000000..58c4fb7 --- /dev/null +++ b/misc.c @@ -0,0 +1,25 @@ +/* + * misc.c: Miscellaneous helpful functions. + */ + +#include +#include + +#include "puzzles.h" + +int rand_upto(int limit) +{ + unsigned long divisor = RAND_MAX / (unsigned)limit; + unsigned long max = divisor * (unsigned)limit; + unsigned long n; + + assert(limit > 0); + + do { + n = rand(); + } while (n >= max); + + n /= divisor; + + return (int)n; +} diff --git a/puzzles.h b/puzzles.h index 5b75697..cb276e2 100644 --- a/puzzles.h +++ b/puzzles.h @@ -37,15 +37,29 @@ typedef struct game_params game_params; typedef struct game_state game_state; typedef struct game_drawstate game_drawstate; +#define ALIGN_VNORMAL 0x000 +#define ALIGN_VCENTRE 0x100 + +#define ALIGN_HLEFT 0x000 +#define ALIGN_HCENTRE 0x001 +#define ALIGN_HRIGHT 0x002 + +#define FONT_FIXED 0 +#define FONT_VARIABLE 1 + /* * Platform routines */ void fatal(char *fmt, ...); void frontend_default_colour(frontend *fe, float *output); +void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize, + int align, int colour, char *text); void draw_rect(frontend *fe, int x, int y, int w, int h, int colour); void draw_line(frontend *fe, int x1, int y1, int x2, int y2, int colour); void draw_polygon(frontend *fe, int *coords, int npoints, int fill, int colour); +void clip(frontend *fe, int x, int y, int w, int h); +void unclip(frontend *fe); void start_draw(frontend *fe); void draw_update(frontend *fe, int x, int y, int w, int h); void end_draw(frontend *fe); @@ -83,6 +97,11 @@ char *dupstr(char *s); #define sresize(array, number, type) \ ( (type *) srealloc ((array), (number) * sizeof (type)) ) +/* + * misc.c + */ +int rand_upto(int limit); + /* * random.c */ diff --git a/sixteen.c b/sixteen.c new file mode 100644 index 0000000..bc21964 --- /dev/null +++ b/sixteen.c @@ -0,0 +1,604 @@ +/* + * sixteen.c: `16-puzzle', a sliding-tiles jigsaw which differs + * from the 15-puzzle in that you toroidally rotate a row or column + * at a time. + */ + +#include +#include +#include +#include +#include + +#include "puzzles.h" + +const char *const game_name = "Sixteen"; + +#define TILE_SIZE 48 +#define BORDER TILE_SIZE /* big border to fill with arrows */ +#define HIGHLIGHT_WIDTH (TILE_SIZE / 20) +#define COORD(x) ( (x) * TILE_SIZE + BORDER ) +#define FROMCOORD(x) ( ((x) - BORDER + 2*TILE_SIZE) / TILE_SIZE - 2 ) + +#define ANIM_TIME 0.1F +#define FLASH_FRAME 0.1F + +#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 completed; +}; + +game_params *default_params(void) +{ + game_params *ret = snew(game_params); + + ret->w = ret->h = 4; + + return ret; +} + +int game_fetch_preset(int i, char **name, game_params **params) +{ + game_params *ret; + int w, h; + char buf[80]; + + switch (i) { + case 0: w = 3, h = 3; break; + case 1: w = 4, h = 3; break; + case 2: w = 4, h = 4; break; + case 3: w = 5, h = 4; break; + case 4: w = 5, h = 5; break; + default: return FALSE; + } + + sprintf(buf, "%dx%d", w, h); + *name = dupstr(buf); + *params = ret = snew(game_params); + ret->w = w; + ret->h = h; + return TRUE; +} + +void free_params(game_params *params) +{ + sfree(params); +} + +game_params *dup_params(game_params *params) +{ + game_params *ret = snew(game_params); + *ret = *params; /* structure copy */ + return ret; +} + +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; +} + +char *new_game_seed(game_params *params) +{ + int stop, n, i, x; + int x1, x2, p1, p2; + 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; + } + + /* + * 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 ? rand_upto(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); + } + } + + /* + * Now construct the game seed, 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]+1); + + 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; +} + +game_state *new_game(game_params *params, char *seed) +{ + 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); + + p = seed; + i = 0; + for (i = 0; i < state->n; i++) { + assert(*p); + state->tiles[i] = atoi(p); + while (*p && *p != ',') + p++; + if (*p) p++; /* eat comma */ + } + assert(!*p); + + state->completed = FALSE; + + return state; +} + +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->completed = state->completed; + + return ret; +} + +void free_game(game_state *state) +{ + sfree(state); +} + +game_state *make_move(game_state *from, int x, int y, int button) +{ + int cx, cy; + int dx, dy, tx, ty, n; + game_state *ret; + + if (button != LEFT_BUTTON) + return NULL; + + cx = FROMCOORD(x); + cy = FROMCOORD(y); + if (cx == -1 && cy >= 0 && cy < from->h) + n = from->w, dx = +1, dy = 0; + else if (cx == from->w && cy >= 0 && cy < from->h) + n = from->w, dx = -1, dy = 0; + else if (cy == -1 && cx >= 0 && cx < from->w) + n = from->h, dy = +1, dx = 0; + else if (cy == from->h && cx >= 0 && cx < from->w) + n = from->h, dy = -1, dx = 0; + else + return NULL; /* invalid click location */ + + ret = dup_game(from); + + do { + cx += dx; + cy += dy; + tx = (cx + dx + from->w) % from->w; + ty = (cy + dy + from->h) % from->h; + ret->tiles[C(ret, cx, cy)] = from->tiles[C(from, tx, ty)]; + } while (--n > 0); + + /* + * See if the game has been completed. + */ + if (!ret->completed) { + ret->completed = TRUE; + for (n = 0; n < ret->n; n++) + if (ret->tiles[n] != n+1) + ret->completed = FALSE; + } + + return ret; +} + +/* ---------------------------------------------------------------------- + * Drawing routines. + */ + +struct game_drawstate { + int started; + int w, h, bgcolour; + int *tiles; +}; + +void game_size(game_params *params, int *x, int *y) +{ + *x = TILE_SIZE * params->w + 2 * BORDER; + *y = TILE_SIZE * params->h + 2 * BORDER; +} + +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; +} + +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; +} + +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 draw_arrow(frontend *fe, int x, int y, int xdx, int xdy) +{ + int coords[14]; + int ydy = -xdx, ydx = xdy; + +#define POINT(n, xx, yy) ( \ + coords[2*(n)+0] = x + (xx)*xdx + (yy)*ydx, \ + coords[2*(n)+1] = y + (xx)*xdy + (yy)*ydy) + + POINT(0, TILE_SIZE / 2, 3 * TILE_SIZE / 4); /* top of arrow */ + POINT(1, 3 * TILE_SIZE / 4, TILE_SIZE / 2); /* right corner */ + POINT(2, 5 * TILE_SIZE / 8, TILE_SIZE / 2); /* right concave */ + POINT(3, 5 * TILE_SIZE / 8, TILE_SIZE / 4); /* bottom right */ + POINT(4, 3 * TILE_SIZE / 8, TILE_SIZE / 4); /* bottom left */ + POINT(5, 3 * TILE_SIZE / 8, TILE_SIZE / 2); /* left concave */ + POINT(6, TILE_SIZE / 4, TILE_SIZE / 2); /* left corner */ + + draw_polygon(fe, coords, 7, TRUE, COL_LOWLIGHT); + draw_polygon(fe, coords, 7, FALSE, COL_TEXT); +} + +void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, + game_state *state, 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); + + /* + * Arrows for making moves. + */ + for (i = 0; i < state->w; i++) { + draw_arrow(fe, COORD(i), COORD(0), +1, 0); + draw_arrow(fe, COORD(i+1), COORD(state->h), -1, 0); + } + for (i = 0; i < state->h; i++) { + draw_arrow(fe, COORD(state->w), COORD(i), 0, +1); + draw_arrow(fe, COORD(0), COORD(i+1), 0, -1); + } + + ds->started = TRUE; + } + + /* + * Now draw each tile. We do this in two passes to make + * animation easy. + */ + + clip(fe, COORD(0), COORD(0), TILE_SIZE*state->w, TILE_SIZE*state->h); + + 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, x2, y2; + + /* + * Figure out what to _actually_ draw, and where to + * draw it. + */ + if (t == -1) { + int x0, y0, x1, y1, dx, dy; + int j; + + /* + * On the first pass, just blank the tile. + */ + if (pass == 0) { + x = COORD(X(state, i)); + y = COORD(Y(state, i)); + x2 = y2 = -1; + t = 0; + } else { + float c; + + t = state->tiles[i]; + + /* + * FIXME: must be prepared to draw a double + * tile in some situations. + */ + + /* + * 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)); + + dx = (x1 - x0); + if (abs(dx) > TILE_SIZE) { + dx = (dx < 0 ? dx + TILE_SIZE * state->w : + dx - TILE_SIZE * state->w); + assert(abs(dx) == TILE_SIZE); + } + dy = (y1 - y0); + if (abs(dy) > TILE_SIZE) { + dy = (dy < 0 ? dy + TILE_SIZE * state->h : + dy - TILE_SIZE * state->h); + assert(abs(dy) == TILE_SIZE); + } + + c = (animtime / ANIM_TIME); + if (c < 0.0F) c = 0.0F; + if (c > 1.0F) c = 1.0F; + + x = x0 + (int)(c * dx); + y = y0 + (int)(c * dy); + x2 = x1 - dx + (int)(c * dx); + y2 = y1 - dy + (int)(c * dy); + } + + } else { + if (pass == 0) + continue; + x = COORD(X(state, i)); + y = COORD(Y(state, i)); + x2 = y2 = -1; + } + + draw_tile(fe, state, x, y, t, bgcolour); + if (x2 != -1 || y2 != -1) + draw_tile(fe, state, x2, y2, t, bgcolour); + } + ds->tiles[i] = t0; + } + } + + unclip(fe); + + ds->bgcolour = bgcolour; +} + +float game_anim_length(game_state *oldstate, game_state *newstate) +{ + return ANIM_TIME; +} + +float game_flash_length(game_state *oldstate, game_state *newstate) +{ + if (!oldstate->completed && newstate->completed) + return 2 * FLASH_FRAME; + else + return 0.0F; +} diff --git a/windows.c b/windows.c index 4db17f4..ea5d7a2 100644 --- a/windows.c +++ b/windows.c @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -63,6 +64,12 @@ void debug_printf(char *fmt, ...) #endif +struct font { + HFONT font; + int type; + int size; +}; + struct frontend { midend_data *me; HWND hwnd; @@ -71,9 +78,12 @@ struct frontend { COLORREF *colours; HBRUSH *brushes; HPEN *pens; + HRGN clip; UINT timer; int npresets; game_params **presets; + struct font *fonts; + int nfonts, fontsize; }; void fatal(char *fmt, ...) @@ -99,6 +109,86 @@ void frontend_default_colour(frontend *fe, float *output) output[2] = (float)(GetBValue(c) / 255.0); } +void clip(frontend *fe, int x, int y, int w, int h) +{ + if (!fe->clip) { + fe->clip = CreateRectRgn(0, 0, 1, 1); + GetClipRgn(fe->hdc_bm, fe->clip); + } + + IntersectClipRect(fe->hdc_bm, x, y, x+w, y+h); +} + +void unclip(frontend *fe) +{ + assert(fe->clip); + SelectClipRgn(fe->hdc_bm, fe->clip); +} + +void draw_text(frontend *fe, int x, int y, int fonttype, int fontsize, + int align, int colour, char *text) +{ + int i; + + /* + * Find or create the font. + */ + for (i = 0; i < fe->nfonts; i++) + if (fe->fonts[i].type == fonttype && fe->fonts[i].size == fontsize) + break; + + if (i == fe->nfonts) { + if (fe->fontsize <= fe->nfonts) { + fe->fontsize = fe->nfonts + 10; + fe->fonts = sresize(fe->fonts, fe->fontsize, struct font); + } + + fe->nfonts++; + + fe->fonts[i].type = fonttype; + fe->fonts[i].size = fontsize; + + /* + * FIXME: Really I should make at least _some_ effort to + * pick the correct font. + */ + fe->fonts[i].font = CreateFont(-fontsize, 0, 0, 0, 0, + FALSE, FALSE, FALSE, DEFAULT_CHARSET, + OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, + DEFAULT_QUALITY, + (fonttype == FONT_FIXED ? + FIXED_PITCH | FF_DONTCARE : + VARIABLE_PITCH | FF_SWISS), + NULL); + } + + /* + * Position and draw the text. + */ + { + HFONT oldfont; + TEXTMETRIC tm; + SIZE size; + + oldfont = SelectObject(fe->hdc_bm, fe->fonts[i].font); + if (GetTextMetrics(fe->hdc_bm, &tm)) { + if (align & ALIGN_VCENTRE) + y -= (tm.tmAscent+tm.tmDescent)/2; + else + y -= tm.tmAscent; + } + if (GetTextExtentPoint32(fe->hdc_bm, text, strlen(text), &size)) { + if (align & ALIGN_HCENTRE) + x -= size.cx / 2; + else if (align & ALIGN_HRIGHT) + x -= size.cx; + } + SetBkMode(fe->hdc_bm, TRANSPARENT); + TextOut(fe->hdc_bm, x, y, text, strlen(text)); + SelectObject(fe->hdc_bm, oldfont); + } +} + void draw_rect(frontend *fe, int x, int y, int w, int h, int colour) { if (w == 1 && h == 1) { @@ -161,6 +251,7 @@ void start_draw(frontend *fe) fe->hdc_bm = CreateCompatibleDC(hdc_win); fe->prevbm = SelectObject(fe->hdc_bm, fe->bitmap); ReleaseDC(fe->hwnd, hdc_win); + fe->clip = NULL; } void draw_update(frontend *fe, int x, int y, int w, int h) @@ -179,6 +270,10 @@ void end_draw(frontend *fe) { SelectObject(fe->hdc_bm, fe->prevbm); DeleteDC(fe->hdc_bm); + if (fe->clip) { + DeleteObject(fe->clip); + fe->clip = NULL; + } } void deactivate_timer(frontend *fe) -- 2.30.2