X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=blobdiff_plain;f=net.c;h=57d91d3581c5af92029ad219e505fa7db3f3c22f;hb=a0a581c8b5422bf0c5ed3fde6aa25811e4eb89fc;hp=33af6c6c7b304d59dfee48d7b323626f1dbdea09;hpb=ad2ec32e1cbe6122b023b29baeaf84a530fcc6a9;p=sgt-puzzles.git diff --git a/net.c b/net.c index 33af6c6..57d91d3 100644 --- a/net.c +++ b/net.c @@ -12,6 +12,21 @@ #include "puzzles.h" #include "tree234.h" +/* + * The standard user interface for Net simply has left- and + * right-button mouse clicks in a square rotate it one way or the + * other. We also provide, by #ifdef, a separate interface based on + * rotational dragging motions. I initially developed this for the + * Mac on the basis that it might work better than the click + * interface with only one mouse button available, but in fact + * found it to be quite strange and unintuitive. Apparently it + * works better on stylus-driven platforms such as Palm and + * PocketPC, though, so we enable it by default there. + */ +#ifdef STYLUS_BASED +#define USE_DRAGGING +#endif + #define MATMUL(xr,yr,m,x,y) do { \ float rx, ry, xx = (x), yy = (y), *mat = (m); \ rx = mat[0] * xx + mat[2] * yy; \ @@ -26,6 +41,11 @@ #define D 0x08 #define LOCKED 0x10 #define ACTIVE 0x20 +#define RLOOP (R << 6) +#define ULOOP (U << 6) +#define LLOOP (L << 6) +#define DLOOP (D << 6) +#define LOOP(dir) ((dir) << 6) /* Rotations: Anticlockwise, Clockwise, Flip, general rotate */ #define A(x) ( (((x) & 0x07) << 1) | (((x) & 0x08) >> 3) ) @@ -43,9 +63,14 @@ #define COUNT(x) ( (((x) & 0x08) >> 3) + (((x) & 0x04) >> 2) + \ (((x) & 0x02) >> 1) + ((x) & 0x01) ) -#define TILE_SIZE 32 +#define PREFERRED_TILE_SIZE 32 +#define TILE_SIZE (ds->tilesize) #define TILE_BORDER 1 +#ifdef SMALL_SCREEN +#define WINDOW_OFFSET 4 +#else #define WINDOW_OFFSET 16 +#endif #define ROTATE_TIME 0.13F #define FLASH_FRAME 0.07F @@ -65,6 +90,7 @@ enum { COL_ENDPOINT, COL_POWERED, COL_BARRIER, + COL_LOOP, NCOLOURS }; @@ -76,15 +102,10 @@ struct game_params { float barrier_probability; }; -struct game_aux_info { - int width, height; - unsigned char *tiles; -}; - struct game_state { int width, height, wrapping, completed; int last_rotate_x, last_rotate_y, last_rotate_dir; - int used_solve, just_used_solve; + int used_solve; unsigned char *tiles; unsigned char *barriers; }; @@ -154,12 +175,16 @@ static const struct game_params net_presets[] = { {7, 7, FALSE, TRUE, 0.0}, {9, 9, FALSE, TRUE, 0.0}, {11, 11, FALSE, TRUE, 0.0}, +#ifndef SMALL_SCREEN {13, 11, FALSE, TRUE, 0.0}, +#endif {5, 5, TRUE, TRUE, 0.0}, {7, 7, TRUE, TRUE, 0.0}, {9, 9, TRUE, TRUE, 0.0}, {11, 11, TRUE, TRUE, 0.0}, +#ifndef SMALL_SCREEN {13, 11, TRUE, TRUE, 0.0}, +#endif }; static int game_fetch_preset(int i, char **name, game_params **params) @@ -186,7 +211,7 @@ static void free_params(game_params *params) sfree(params); } -static game_params *dup_params(game_params *params) +static game_params *dup_params(const game_params *params) { game_params *ret = snew(game_params); *ret = *params; /* structure copy */ @@ -213,7 +238,7 @@ static void decode_params(game_params *ret, char const *string) ret->wrapping = TRUE; } else if (*p == 'b') { p++; - ret->barrier_probability = atof(p); + ret->barrier_probability = (float)atof(p); while (*p && (*p == '.' || isdigit((unsigned char)*p))) p++; } else if (*p == 'a') { p++; @@ -223,7 +248,7 @@ static void decode_params(game_params *ret, char const *string) } } -static char *encode_params(game_params *params, int full) +static char *encode_params(const game_params *params, int full) { char ret[400]; int len; @@ -241,7 +266,7 @@ static char *encode_params(game_params *params, int full) return dupstr(ret); } -static config_item *game_configure(game_params *params) +static config_item *game_configure(const game_params *params) { config_item *ret; char buf[80]; @@ -284,7 +309,7 @@ static config_item *game_configure(game_params *params) return ret; } -static game_params *custom_params(config_item *cfg) +static game_params *custom_params(const config_item *cfg) { game_params *ret = snew(game_params); @@ -297,7 +322,7 @@ static game_params *custom_params(config_item *cfg) return ret; } -static char *validate_params(game_params *params) +static char *validate_params(const game_params *params, int full) { if (params->width <= 0 || params->height <= 0) return "Width and height must both be greater than zero"; @@ -351,7 +376,7 @@ static char *validate_params(game_params *params) * is at least 2^(number of such rows), and in particular is at * least 2 since there must be at least one such row. [] */ - if (params->unique && params->wrapping && + if (full && params->unique && params->wrapping && (params->width == 2 || params->height == 2)) return "No wrapping puzzle with a width or height of 2 can have" " a unique solution"; @@ -386,29 +411,6 @@ static char *validate_params(game_params *params) * avoidance is required. */ -static int dsf_canonify(int *dsf, int val) -{ - int v2 = val; - - while (dsf[val] != val) - val = dsf[val]; - - while (v2 != val) { - int tmp = dsf[v2]; - dsf[v2] = val; - v2 = tmp; - } - - return val; -} - -static void dsf_merge(int *dsf, int v1, int v2) -{ - v1 = dsf_canonify(dsf, v1); - v2 = dsf_canonify(dsf, v2); - dsf[v2] = v1; -} - struct todo { unsigned char *marked; int *buffer; @@ -457,6 +459,11 @@ static int todo_get(struct todo *todo) { return ret; } +/* + * Return values: -1 means puzzle was proved inconsistent, 0 means we + * failed to narrow down to a unique solution, +1 means we solved it + * fully. + */ static int net_solver(int w, int h, unsigned char *tiles, unsigned char *barriers, int wrapping) { @@ -548,9 +555,7 @@ static int net_solver(int w, int h, unsigned char *tiles, * classes) by finding the representative of each tile and * setting equivalence[one]=the_other. */ - equivalence = snewn(w * h, int); - for (i = 0; i < w*h; i++) - equivalence[i] = i; /* initially all distinct */ + equivalence = snew_dsf(w * h); /* * On a non-wrapping grid, we instantly know that all the edges @@ -733,7 +738,11 @@ static int net_solver(int w, int h, unsigned char *tiles, #endif } - assert(j > 0); /* we can't lose _all_ possibilities! */ + if (j == 0) { + /* If we've ruled out all possible orientations for a + * tile, then our puzzle has no solution at all. */ + return -1; + } if (j < i) { done_something = TRUE; @@ -813,14 +822,14 @@ static int net_solver(int w, int h, unsigned char *tiles, /* * Mark all completely determined tiles as locked. */ - j = TRUE; + j = +1; for (i = 0; i < w*h; i++) { if (tilestate[i * 4 + 1] == 255) { assert(tilestate[i * 4 + 0] != 255); tiles[i] = tilestate[i * 4] | LOCKED; } else { tiles[i] &= ~LOCKED; - j = FALSE; + j = 0; } } @@ -924,14 +933,7 @@ static void perturb(int w, int h, unsigned char *tiles, int wrapping, perim2 = snewn(nperim, struct xyd); memcpy(perim2, perimeter, nperim * sizeof(struct xyd)); /* Shuffle the perimeter, so as to search it without directional bias. */ - for (i = nperim; --i ;) { - int j = random_upto(rs, i+1); - struct xyd t; - - t = perim2[j]; - perim2[j] = perim2[i]; - perim2[i] = t; - } + shuffle(perim2, nperim, sizeof(*perim2), rs); for (i = 0; i < nperim; i++) { int x2, y2; @@ -963,8 +965,10 @@ static void perturb(int w, int h, unsigned char *tiles, int wrapping, } sfree(perim2); - if (i == nperim) + if (i == nperim) { + sfree(perimeter); return; /* nothing we can do! */ + } /* * Now we've constructed a new link, we need to find the entire @@ -1137,8 +1141,12 @@ static void perturb(int w, int h, unsigned char *tiles, int wrapping, sfree(perimeter); } -static char *new_game_desc(game_params *params, random_state *rs, - game_aux_info **aux, int interactive) +static int *compute_loops_inner(int w, int h, int wrapping, + const unsigned char *tiles, + const unsigned char *barriers); + +static char *new_game_desc(const game_params *params, random_state *rs, + char **aux, int interactive) { tree234 *possibilities, *barriertree; int w, h, x, y, cx, cy, nbarriers; @@ -1229,7 +1237,7 @@ static char *new_game_desc(game_params *params, random_state *rs, OFFSET(x2, y2, x1, y1, d1, params); d2 = F(d1); -#ifdef DEBUG +#ifdef GENERATION_DIAGNOSTICS printf("picked (%d,%d,%c) <-> (%d,%d,%c)\n", x1, y1, "0RU3L567D9abcdef"[d1], x2, y2, "0RU3L567D9abcdef"[d2]); #endif @@ -1256,7 +1264,7 @@ static char *new_game_desc(game_params *params, random_state *rs, xydp = find234(possibilities, &xyd1, NULL); if (xydp) { -#ifdef DEBUG +#ifdef GENERATION_DIAGNOSTICS printf("T-piece; removing (%d,%d,%c)\n", xydp->x, xydp->y, "0RU3L567D9abcdef"[xydp->direction]); #endif @@ -1283,7 +1291,7 @@ static char *new_game_desc(game_params *params, random_state *rs, xydp = find234(possibilities, &xyd1, NULL); if (xydp) { -#ifdef DEBUG +#ifdef GENERATION_DIAGNOSTICS printf("Loop avoidance; removing (%d,%d,%c)\n", xydp->x, xydp->y, "0RU3L567D9abcdef"[xydp->direction]); #endif @@ -1318,7 +1326,7 @@ static char *new_game_desc(game_params *params, random_state *rs, if (index(params, tiles, x3, y3)) continue; /* this would create a loop */ -#ifdef DEBUG +#ifdef GENERATION_DIAGNOSTICS printf("New frontier; adding (%d,%d,%c)\n", x2, y2, "0RU3L567D9abcdef"[d]); #endif @@ -1335,7 +1343,7 @@ static char *new_game_desc(game_params *params, random_state *rs, /* * Run the solver to check unique solubility. */ - while (!net_solver(w, h, tiles, NULL, params->wrapping)) { + while (net_solver(w, h, tiles, NULL, params->wrapping) != 1) { int n = 0; /* @@ -1400,29 +1408,114 @@ static char *new_game_desc(game_params *params, random_state *rs, } /* - * Save the unshuffled grid in an aux_info. + * Save the unshuffled grid in aux. */ { - game_aux_info *solution; + char *solution; + int i; - solution = snew(game_aux_info); - solution->width = w; - solution->height = h; - solution->tiles = snewn(w * h, unsigned char); - memcpy(solution->tiles, tiles, w * h); + solution = snewn(w * h + 1, char); + for (i = 0; i < w * h; i++) + solution[i] = "0123456789abcdef"[tiles[i] & 0xF]; + solution[w*h] = '\0'; *aux = solution; } /* * Now shuffle the grid. + * + * In order to avoid accidentally generating an already-solved + * grid, we will reshuffle as necessary to ensure that at least + * one edge has a mismatched connection. + * + * This can always be done, since validate_params() enforces a + * grid area of at least 2 and our generator never creates + * either type of rotationally invariant tile (cross and + * blank). Hence there must be at least one edge separating + * distinct tiles, and it must be possible to find orientations + * of those tiles such that one tile is trying to connect + * through that edge and the other is not. + * + * (We could be more subtle, and allow the shuffle to generate + * a grid in which all tiles match up locally and the only + * criterion preventing the grid from being already solved is + * connectedness. However, that would take more effort, and + * it's easier to simply make sure every grid is _obviously_ + * not solved.) + * + * We also require that our shuffle produces no loops in the + * initial grid state, because it's a bit rude to light up a 'HEY, + * YOU DID SOMETHING WRONG!' indicator when the user hasn't even + * had a chance to do _anything_ yet. This also is possible just + * by retrying the whole shuffle on failure, because it's clear + * that at least one non-solved shuffle with no loops must exist. + * (Proof: take the _solved_ state of the puzzle, and rotate one + * endpoint.) */ - for (y = 0; y < h; y++) { - for (x = 0; x < w; x++) { - int orig = index(params, tiles, x, y); - int rot = random_upto(rs, 4); - index(params, tiles, x, y) = ROT(orig, rot); - } + while (1) { + int mismatches, prev_loopsquares, this_loopsquares, i; + int *loops; + + shuffle: + for (y = 0; y < h; y++) { + for (x = 0; x < w; x++) { + int orig = index(params, tiles, x, y); + int rot = random_upto(rs, 4); + index(params, tiles, x, y) = ROT(orig, rot); + } + } + + /* + * Check for loops, and try to fix them by reshuffling just + * the squares involved. + */ + prev_loopsquares = w*h+1; + while (1) { + loops = compute_loops_inner(w, h, params->wrapping, tiles, NULL); + this_loopsquares = 0; + for (i = 0; i < w*h; i++) { + if (loops[i]) { + int orig = tiles[i]; + int rot = random_upto(rs, 4); + tiles[i] = ROT(orig, rot); + this_loopsquares++; + } + } + sfree(loops); + if (this_loopsquares > prev_loopsquares) { + /* + * We're increasing rather than reducing the number of + * loops. Give up and go back to the full shuffle. + */ + goto shuffle; + } + if (this_loopsquares == 0) + break; + prev_loopsquares = this_loopsquares; + } + + mismatches = 0; + /* + * I can't even be bothered to check for mismatches across + * a wrapping edge, so I'm just going to enforce that there + * must be a mismatch across a non-wrapping edge, which is + * still always possible. + */ + for (y = 0; y < h; y++) for (x = 0; x < w; x++) { + if (x+1 < w && ((ROT(index(params, tiles, x, y), 2) ^ + index(params, tiles, x+1, y)) & L)) + mismatches++; + if (y+1 < h && ((ROT(index(params, tiles, x, y), 2) ^ + index(params, tiles, x, y+1)) & U)) + mismatches++; + } + + if (mismatches == 0) + continue; + + /* OK. */ + break; } /* @@ -1514,13 +1607,7 @@ static char *new_game_desc(game_params *params, random_state *rs, return desc; } -static void game_free_aux_info(game_aux_info *aux) -{ - sfree(aux->tiles); - sfree(aux); -} - -static char *validate_desc(game_params *params, char *desc) +static char *validate_desc(const game_params *params, const char *desc) { int w = params->width, h = params->height; int i; @@ -1550,7 +1637,8 @@ static char *validate_desc(game_params *params, char *desc) * Construct an initial game state, given a description and parameters. */ -static game_state *new_game(midend_data *me, game_params *params, char *desc) +static game_state *new_game(midend *me, const game_params *params, + const char *desc) { game_state *state; int w, h, x, y; @@ -1566,7 +1654,7 @@ static game_state *new_game(midend_data *me, game_params *params, char *desc) h = state->height = params->height; state->wrapping = params->wrapping; state->last_rotate_dir = state->last_rotate_x = state->last_rotate_y = 0; - state->completed = state->used_solve = state->just_used_solve = FALSE; + state->completed = state->used_solve = FALSE; state->tiles = snewn(state->width * state->height, unsigned char); memset(state->tiles, 0, state->width * state->height); state->barriers = snewn(state->width * state->height, unsigned char); @@ -1627,7 +1715,7 @@ static game_state *new_game(midend_data *me, game_params *params, char *desc) if (!(barrier(state, x, 0) & U) || !(barrier(state, x, state->height-1) & D)) state->wrapping = TRUE; - for (y = 0; y < state->width; y++) + for (y = 0; y < state->height; y++) if (!(barrier(state, 0, y) & L) || !(barrier(state, state->width-1, y) & R)) state->wrapping = TRUE; @@ -1636,7 +1724,7 @@ static game_state *new_game(midend_data *me, game_params *params, char *desc) return state; } -static game_state *dup_game(game_state *state) +static game_state *dup_game(const game_state *state) { game_state *ret; @@ -1646,7 +1734,6 @@ static game_state *dup_game(game_state *state) ret->wrapping = state->wrapping; ret->completed = state->completed; ret->used_solve = state->used_solve; - ret->just_used_solve = state->just_used_solve; ret->last_rotate_dir = state->last_rotate_dir; ret->last_rotate_x = state->last_rotate_x; ret->last_rotate_y = state->last_rotate_y; @@ -1665,32 +1752,115 @@ static void free_game(game_state *state) sfree(state); } -static game_state *solve_game(game_state *state, game_aux_info *aux, - char **error) +static char *solve_game(const game_state *state, const game_state *currstate, + const char *aux, char **error) { - game_state *ret; + unsigned char *tiles; + char *ret; + int retlen, retsize; + int i; + + tiles = snewn(state->width * state->height, unsigned char); if (!aux) { /* * Run the internal solver on the provided grid. This might * not yield a complete solution. */ - ret = dup_game(state); - net_solver(ret->width, ret->height, ret->tiles, - ret->barriers, ret->wrapping); + int solver_result; + + memcpy(tiles, state->tiles, state->width * state->height); + solver_result = net_solver(state->width, state->height, tiles, + state->barriers, state->wrapping); + + if (solver_result < 0) { + *error = "No solution exists for this puzzle"; + sfree(tiles); + return NULL; + } } else { - assert(aux->width == state->width); - assert(aux->height == state->height); - ret = dup_game(state); - memcpy(ret->tiles, aux->tiles, ret->width * ret->height); - ret->used_solve = ret->just_used_solve = TRUE; - ret->completed = TRUE; + for (i = 0; i < state->width * state->height; i++) { + int c = aux[i]; + + if (c >= '0' && c <= '9') + tiles[i] = c - '0'; + else if (c >= 'a' && c <= 'f') + tiles[i] = c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + tiles[i] = c - 'A' + 10; + + tiles[i] |= LOCKED; + } + } + + /* + * Now construct a string which can be passed to execute_move() + * to transform the current grid into the solved one. + */ + retsize = 256; + ret = snewn(retsize, char); + retlen = 0; + ret[retlen++] = 'S'; + + for (i = 0; i < state->width * state->height; i++) { + int from = currstate->tiles[i], to = tiles[i]; + int ft = from & (R|L|U|D), tt = to & (R|L|U|D); + int x = i % state->width, y = i / state->width; + int chr = '\0'; + char buf[80], *p = buf; + + if (from == to) + continue; /* nothing needs doing at all */ + + /* + * To transform this tile into the desired tile: first + * unlock the tile if it's locked, then rotate it if + * necessary, then lock it if necessary. + */ + if (from & LOCKED) + p += sprintf(p, ";L%d,%d", x, y); + + if (tt == A(ft)) + chr = 'A'; + else if (tt == C(ft)) + chr = 'C'; + else if (tt == F(ft)) + chr = 'F'; + else { + assert(tt == ft); + chr = '\0'; + } + if (chr) + p += sprintf(p, ";%c%d,%d", chr, x, y); + + if (to & LOCKED) + p += sprintf(p, ";L%d,%d", x, y); + + if (p > buf) { + if (retlen + (p - buf) >= retsize) { + retsize = retlen + (p - buf) + 512; + ret = sresize(ret, retsize, char); + } + memcpy(ret+retlen, buf, p - buf); + retlen += p - buf; + } } + assert(retlen < retsize); + ret[retlen] = '\0'; + ret = sresize(ret, retlen+1, char); + + sfree(tiles); + return ret; } -static char *game_text_format(game_state *state) +static int game_can_format_as_text_now(const game_params *params) +{ + return TRUE; +} + +static char *game_text_format(const game_state *state) { return NULL; } @@ -1706,7 +1876,7 @@ static char *game_text_format(game_state *state) * completed - just call this function and see whether every square * is marked active. */ -static unsigned char *compute_active(game_state *state, int cx, int cy) +static unsigned char *compute_active(const game_state *state, int cx, int cy) { unsigned char *active; tree234 *todo; @@ -1756,15 +1926,99 @@ static unsigned char *compute_active(game_state *state, int cx, int cy) return active; } +struct net_neighbour_ctx { + int w, h; + const unsigned char *tiles, *barriers; + int i, n, neighbours[4]; +}; +static int net_neighbour(int vertex, void *vctx) +{ + struct net_neighbour_ctx *ctx = (struct net_neighbour_ctx *)vctx; + + if (vertex >= 0) { + int x = vertex % ctx->w, y = vertex / ctx->w; + int tile, dir, x1, y1, v1; + + ctx->i = ctx->n = 0; + + tile = ctx->tiles[vertex]; + if (ctx->barriers) + tile &= ~ctx->barriers[vertex]; + + for (dir = 1; dir < 0x10; dir <<= 1) { + if (!(tile & dir)) + continue; + OFFSETWH(x1, y1, x, y, dir, ctx->w, ctx->h); + v1 = y1 * ctx->w + x1; + if (ctx->tiles[v1] & F(dir)) + ctx->neighbours[ctx->n++] = v1; + } + } + + if (ctx->i < ctx->n) + return ctx->neighbours[ctx->i++]; + else + return -1; +} + +static int *compute_loops_inner(int w, int h, int wrapping, + const unsigned char *tiles, + const unsigned char *barriers) +{ + struct net_neighbour_ctx ctx; + struct findloopstate *fls; + int *loops; + int x, y; + + fls = findloop_new_state(w*h); + ctx.w = w; + ctx.h = h; + ctx.tiles = tiles; + ctx.barriers = barriers; + findloop_run(fls, w*h, net_neighbour, &ctx); + + loops = snewn(w*h, int); + + for (y = 0; y < h; y++) { + for (x = 0; x < w; x++) { + int x1, y1, dir; + int flags = 0; + + for (dir = 1; dir < 0x10; dir <<= 1) { + if ((tiles[y*w+x] & dir) && + !(barriers && (barriers[y*w+x] & dir))) { + OFFSETWH(x1, y1, x, y, dir, w, h); + if ((tiles[y1*w+x1] & F(dir)) && + findloop_is_loop_edge(fls, y*w+x, y1*w+x1)) + flags |= LOOP(dir); + } + } + loops[y*w+x] = flags; + } + } + + findloop_free_state(fls); + return loops; +} + +static int *compute_loops(const game_state *state) +{ + return compute_loops_inner(state->width, state->height, state->wrapping, + state->tiles, state->barriers); +} + struct game_ui { int org_x, org_y; /* origin */ int cx, cy; /* source tile (game coordinates) */ int cur_x, cur_y; int cur_visible; random_state *rs; /* used for jumbling */ +#ifdef USE_DRAGGING + int dragtilex, dragtiley, dragstartx, dragstarty, dragged; +#endif }; -static game_ui *new_ui(game_state *state) +static game_ui *new_ui(const game_state *state) { void *seed; int seedsize; @@ -1774,7 +2028,7 @@ static game_ui *new_ui(game_state *state) ui->cur_y = ui->cy = state->height / 2; ui->cur_visible = FALSE; get_random_seed(&seed, &seedsize); - ui->rs = random_init(seed, seedsize); + ui->rs = random_new(seed, seedsize); sfree(seed); return ui; @@ -1786,25 +2040,68 @@ static void free_ui(game_ui *ui) sfree(ui); } +static char *encode_ui(const game_ui *ui) +{ + char buf[120]; + /* + * We preserve the origin and centre-point coordinates over a + * serialise. + */ + sprintf(buf, "O%d,%d;C%d,%d", ui->org_x, ui->org_y, ui->cx, ui->cy); + return dupstr(buf); +} + +static void decode_ui(game_ui *ui, const char *encoding) +{ + sscanf(encoding, "O%d,%d;C%d,%d", + &ui->org_x, &ui->org_y, &ui->cx, &ui->cy); +} + +static void game_changed_state(game_ui *ui, const game_state *oldstate, + const game_state *newstate) +{ +} + +struct game_drawstate { + int started; + int width, height; + int org_x, org_y; + int tilesize; + int *visible; +}; + /* ---------------------------------------------------------------------- * Process a move. */ -static game_state *make_move(game_state *state, game_ui *ui, - game_drawstate *ds, int x, int y, int button) { - game_state *ret, *nullret; - int tx, ty, orig; +static char *interpret_move(const game_state *state, game_ui *ui, + const game_drawstate *ds, + int x, int y, int button) +{ + char *nullret; + int tx = -1, ty = -1, dir = 0; int shift = button & MOD_SHFT, ctrl = button & MOD_CTRL; + enum { + NONE, ROTATE_LEFT, ROTATE_180, ROTATE_RIGHT, TOGGLE_LOCK, JUMBLE, + MOVE_ORIGIN, MOVE_SOURCE, MOVE_ORIGIN_AND_SOURCE, MOVE_CURSOR + } action; button &= ~MOD_MASK; nullret = NULL; + action = NONE; if (button == LEFT_BUTTON || button == MIDDLE_BUTTON || +#ifdef USE_DRAGGING + button == LEFT_DRAG || + button == LEFT_RELEASE || + button == RIGHT_DRAG || + button == RIGHT_RELEASE || +#endif button == RIGHT_BUTTON) { if (ui->cur_visible) { ui->cur_visible = FALSE; - nullret = state; + nullret = ""; } /* @@ -1824,53 +2121,143 @@ static game_state *make_move(game_state *state, game_ui *ui, if (x % TILE_SIZE >= TILE_SIZE - TILE_BORDER || y % TILE_SIZE >= TILE_SIZE - TILE_BORDER) return nullret; - } else if (button == CURSOR_UP || button == CURSOR_DOWN || - button == CURSOR_RIGHT || button == CURSOR_LEFT) { - int dir; - switch (button) { - case CURSOR_UP: dir = U; break; - case CURSOR_DOWN: dir = D; break; - case CURSOR_LEFT: dir = L; break; - case CURSOR_RIGHT: dir = R; break; - default: return nullret; - } - if (shift) { + +#ifdef USE_DRAGGING + + if (button == MIDDLE_BUTTON +#ifdef STYLUS_BASED + || button == RIGHT_BUTTON /* with a stylus, `right-click' locks */ +#endif + ) { /* - * Move origin. + * Middle button never drags: it only toggles the lock. */ - if (state->wrapping) { - OFFSET(ui->org_x, ui->org_y, ui->org_x, ui->org_y, dir, state); - } else return nullret; /* disallowed for non-wrapping grids */ - } - if (ctrl) { + action = TOGGLE_LOCK; + } else if (button == LEFT_BUTTON +#ifndef STYLUS_BASED + || button == RIGHT_BUTTON /* (see above) */ +#endif + ) { /* - * Change source tile. + * Otherwise, we note down the start point for a drag. */ - OFFSET(ui->cx, ui->cy, ui->cx, ui->cy, dir, state); - } - if (!shift && !ctrl) { + ui->dragtilex = tx; + ui->dragtiley = ty; + ui->dragstartx = x % TILE_SIZE; + ui->dragstarty = y % TILE_SIZE; + ui->dragged = FALSE; + return nullret; /* no actual action */ + } else if (button == LEFT_DRAG +#ifndef STYLUS_BASED + || button == RIGHT_DRAG +#endif + ) { /* - * Move keyboard cursor. + * Find the new drag point and see if it necessitates a + * rotation. */ - OFFSET(ui->cur_x, ui->cur_y, ui->cur_x, ui->cur_y, dir, state); - ui->cur_visible = TRUE; + int x0,y0, xA,yA, xC,yC, xF,yF; + int mx, my; + int d0, dA, dC, dF, dmin; + + tx = ui->dragtilex; + ty = ui->dragtiley; + + mx = x - (ui->dragtilex * TILE_SIZE); + my = y - (ui->dragtiley * TILE_SIZE); + + x0 = ui->dragstartx; + y0 = ui->dragstarty; + xA = ui->dragstarty; + yA = TILE_SIZE-1 - ui->dragstartx; + xF = TILE_SIZE-1 - ui->dragstartx; + yF = TILE_SIZE-1 - ui->dragstarty; + xC = TILE_SIZE-1 - ui->dragstarty; + yC = ui->dragstartx; + + d0 = (mx-x0)*(mx-x0) + (my-y0)*(my-y0); + dA = (mx-xA)*(mx-xA) + (my-yA)*(my-yA); + dF = (mx-xF)*(mx-xF) + (my-yF)*(my-yF); + dC = (mx-xC)*(mx-xC) + (my-yC)*(my-yC); + + dmin = min(min(d0,dA),min(dF,dC)); + + if (d0 == dmin) { + return nullret; + } else if (dF == dmin) { + action = ROTATE_180; + ui->dragstartx = xF; + ui->dragstarty = yF; + ui->dragged = TRUE; + } else if (dA == dmin) { + action = ROTATE_LEFT; + ui->dragstartx = xA; + ui->dragstarty = yA; + ui->dragged = TRUE; + } else /* dC == dmin */ { + action = ROTATE_RIGHT; + ui->dragstartx = xC; + ui->dragstarty = yC; + ui->dragged = TRUE; + } + } else if (button == LEFT_RELEASE +#ifndef STYLUS_BASED + || button == RIGHT_RELEASE +#endif + ) { + if (!ui->dragged) { + /* + * There was a click but no perceptible drag: + * revert to single-click behaviour. + */ + tx = ui->dragtilex; + ty = ui->dragtiley; + + if (button == LEFT_RELEASE) + action = ROTATE_LEFT; + else + action = ROTATE_RIGHT; + } else + return nullret; /* no action */ } - return state; /* UI activity has occurred */ + +#else /* USE_DRAGGING */ + + action = (button == LEFT_BUTTON ? ROTATE_LEFT : + button == RIGHT_BUTTON ? ROTATE_RIGHT : TOGGLE_LOCK); + +#endif /* USE_DRAGGING */ + + } else if (IS_CURSOR_MOVE(button)) { + switch (button) { + case CURSOR_UP: dir = U; break; + case CURSOR_DOWN: dir = D; break; + case CURSOR_LEFT: dir = L; break; + case CURSOR_RIGHT: dir = R; break; + default: return nullret; + } + if (shift && ctrl) action = MOVE_ORIGIN_AND_SOURCE; + else if (shift) action = MOVE_ORIGIN; + else if (ctrl) action = MOVE_SOURCE; + else action = MOVE_CURSOR; } else if (button == 'a' || button == 's' || button == 'd' || - button == 'A' || button == 'S' || button == 'D') { + button == 'A' || button == 'S' || button == 'D' || + button == 'f' || button == 'F' || + IS_CURSOR_SELECT(button)) { tx = ui->cur_x; ty = ui->cur_y; - if (button == 'a' || button == 'A') - button = LEFT_BUTTON; - else if (button == 's' || button == 'S') - button = MIDDLE_BUTTON; + if (button == 'a' || button == 'A' || button == CURSOR_SELECT) + action = ROTATE_LEFT; + else if (button == 's' || button == 'S' || button == CURSOR_SELECT2) + action = TOGGLE_LOCK; else if (button == 'd' || button == 'D') - button = RIGHT_BUTTON; + action = ROTATE_RIGHT; + else if (button == 'f' || button == 'F') + action = ROTATE_180; ui->cur_visible = TRUE; } else if (button == 'j' || button == 'J') { /* XXX should we have some mouse control for this? */ - button = 'J'; /* canonify */ - tx = ty = -1; /* shut gcc up :( */ + action = JUMBLE; } else return nullret; @@ -1884,15 +2271,13 @@ static game_state *make_move(game_state *state, game_ui *ui, * accident. If they change their mind, another middle click * unlocks it.) */ - if (button == MIDDLE_BUTTON) { - - ret = dup_game(state); - ret->just_used_solve = FALSE; - tile(ret, tx, ty) ^= LOCKED; - ret->last_rotate_dir = ret->last_rotate_x = ret->last_rotate_y = 0; - return ret; - - } else if (button == LEFT_BUTTON || button == RIGHT_BUTTON) { + if (action == TOGGLE_LOCK) { + char buf[80]; + sprintf(buf, "L%d,%d", tx, ty); + return dupstr(buf); + } else if (action == ROTATE_LEFT || action == ROTATE_RIGHT || + action == ROTATE_180) { + char buf[80]; /* * The left and right buttons have no effect if clicked on a @@ -1905,61 +2290,148 @@ static game_state *make_move(game_state *state, game_ui *ui, * Otherwise, turn the tile one way or the other. Left button * turns anticlockwise; right button turns clockwise. */ - ret = dup_game(state); - ret->just_used_solve = FALSE; - orig = tile(ret, tx, ty); - if (button == LEFT_BUTTON) { - tile(ret, tx, ty) = A(orig); - ret->last_rotate_dir = +1; - } else { - tile(ret, tx, ty) = C(orig); - ret->last_rotate_dir = -1; - } - ret->last_rotate_x = tx; - ret->last_rotate_y = ty; - - } else if (button == 'J') { - + sprintf(buf, "%c%d,%d", (int)(action == ROTATE_LEFT ? 'A' : + action == ROTATE_RIGHT ? 'C' : 'F'), tx, ty); + return dupstr(buf); + } else if (action == JUMBLE) { /* * Jumble all unlocked tiles to random orientations. */ - int jx, jy; - ret = dup_game(state); - ret->just_used_solve = FALSE; - for (jy = 0; jy < ret->height; jy++) { - for (jx = 0; jx < ret->width; jx++) { - if (!(tile(ret, jx, jy) & LOCKED)) { + + int jx, jy, maxlen; + char *ret, *p; + + /* + * Maximum string length assumes no int can be converted to + * decimal and take more than 11 digits! + */ + maxlen = state->width * state->height * 25 + 3; + + ret = snewn(maxlen, char); + p = ret; + *p++ = 'J'; + + for (jy = 0; jy < state->height; jy++) { + for (jx = 0; jx < state->width; jx++) { + if (!(tile(state, jx, jy) & LOCKED)) { int rot = random_upto(ui->rs, 4); - orig = tile(ret, jx, jy); - tile(ret, jx, jy) = ROT(orig, rot); + if (rot) { + p += sprintf(p, ";%c%d,%d", "AFC"[rot-1], jx, jy); + } } } } - ret->last_rotate_dir = 0; /* suppress animation */ - ret->last_rotate_x = ret->last_rotate_y = 0; + *p++ = '\0'; + assert(p - ret < maxlen); + ret = sresize(ret, p - ret, char); + return ret; + } else if (action == MOVE_ORIGIN || action == MOVE_SOURCE || + action == MOVE_ORIGIN_AND_SOURCE || action == MOVE_CURSOR) { + assert(dir != 0); + if (action == MOVE_ORIGIN || action == MOVE_ORIGIN_AND_SOURCE) { + if (state->wrapping) { + OFFSET(ui->org_x, ui->org_y, ui->org_x, ui->org_y, dir, state); + } else return nullret; /* disallowed for non-wrapping grids */ + } + if (action == MOVE_SOURCE || action == MOVE_ORIGIN_AND_SOURCE) { + OFFSET(ui->cx, ui->cy, ui->cx, ui->cy, dir, state); + } + if (action == MOVE_CURSOR) { + OFFSET(ui->cur_x, ui->cur_y, ui->cur_x, ui->cur_y, dir, state); + ui->cur_visible = TRUE; + } + return ""; } else { - ret = NULL; /* placate optimisers which don't understand assert(0) */ - assert(0); + return NULL; + } +} + +static game_state *execute_move(const game_state *from, const char *move) +{ + game_state *ret; + int tx = -1, ty = -1, n, noanim, orig; + + ret = dup_game(from); + + if (move[0] == 'J' || move[0] == 'S') { + if (move[0] == 'S') + ret->used_solve = TRUE; + + move++; + if (*move == ';') + move++; + noanim = TRUE; + } else + noanim = FALSE; + + ret->last_rotate_dir = 0; /* suppress animation */ + ret->last_rotate_x = ret->last_rotate_y = 0; + + while (*move) { + if ((move[0] == 'A' || move[0] == 'C' || + move[0] == 'F' || move[0] == 'L') && + sscanf(move+1, "%d,%d%n", &tx, &ty, &n) >= 2 && + tx >= 0 && tx < from->width && ty >= 0 && ty < from->height) { + orig = tile(ret, tx, ty); + if (move[0] == 'A') { + tile(ret, tx, ty) = A(orig); + if (!noanim) + ret->last_rotate_dir = +1; + } else if (move[0] == 'F') { + tile(ret, tx, ty) = F(orig); + if (!noanim) + ret->last_rotate_dir = +2; /* + for sake of argument */ + } else if (move[0] == 'C') { + tile(ret, tx, ty) = C(orig); + if (!noanim) + ret->last_rotate_dir = -1; + } else { + assert(move[0] == 'L'); + tile(ret, tx, ty) ^= LOCKED; + } + + move += 1 + n; + if (*move == ';') move++; + } else { + free_game(ret); + return NULL; + } + } + if (!noanim) { + if (tx == -1 || ty == -1) { free_game(ret); return NULL; } + ret->last_rotate_x = tx; + ret->last_rotate_y = ty; } /* * Check whether the game has been completed. + * + * For this purpose it doesn't matter where the source square is, + * because we can start from anywhere (or, at least, any square + * that's non-empty!), and correctly determine whether the game is + * completed. */ { - unsigned char *active = compute_active(ret, ui->cx, ui->cy); - int x1, y1; + unsigned char *active; + int pos; int complete = TRUE; - for (x1 = 0; x1 < ret->width; x1++) - for (y1 = 0; y1 < ret->height; y1++) - if ((tile(ret, x1, y1) & 0xF) && !index(ret, active, x1, y1)) { + for (pos = 0; pos < ret->width * ret->height; pos++) + if (ret->tiles[pos] & 0xF) + break; + + if (pos < ret->width * ret->height) { + active = compute_active(ret, pos % ret->width, pos / ret->width); + + for (pos = 0; pos < ret->width * ret->height; pos++) + if ((ret->tiles[pos] & 0xF) && !active[pos]) { complete = FALSE; - goto break_label; /* break out of two loops at once */ - } - break_label: + break; + } - sfree(active); + sfree(active); + } if (complete) ret->completed = TRUE; @@ -1968,44 +2440,48 @@ static game_state *make_move(game_state *state, game_ui *ui, return ret; } + /* ---------------------------------------------------------------------- * Routines for drawing the game position on the screen. */ -struct game_drawstate { - int started; - int width, height; - int org_x, org_y; - unsigned char *visible; -}; - -static game_drawstate *game_new_drawstate(game_state *state) +static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state) { game_drawstate *ds = snew(game_drawstate); + int i; ds->started = FALSE; ds->width = state->width; ds->height = state->height; ds->org_x = ds->org_y = -1; - ds->visible = snewn(state->width * state->height, unsigned char); - memset(ds->visible, 0xFF, state->width * state->height); + ds->visible = snewn(state->width * state->height, int); + ds->tilesize = 0; /* undecided yet */ + for (i = 0; i < state->width * state->height; i++) + ds->visible[i] = -1; return ds; } -static void game_free_drawstate(game_drawstate *ds) +static void game_free_drawstate(drawing *dr, game_drawstate *ds) { sfree(ds->visible); sfree(ds); } -static void game_size(game_params *params, int *x, int *y) +static void game_compute_size(const game_params *params, int tilesize, + int *x, int *y) +{ + *x = WINDOW_OFFSET * 2 + tilesize * params->width + TILE_BORDER; + *y = WINDOW_OFFSET * 2 + tilesize * params->height + TILE_BORDER; +} + +static void game_set_size(drawing *dr, game_drawstate *ds, + const game_params *params, int tilesize) { - *x = WINDOW_OFFSET * 2 + TILE_SIZE * params->width + TILE_BORDER; - *y = WINDOW_OFFSET * 2 + TILE_SIZE * params->height + TILE_BORDER; + ds->tilesize = tilesize; } -static float *game_colours(frontend *fe, game_state *state, int *ncolours) +static float *game_colours(frontend *fe, int *ncolours) { float *ret; @@ -2039,6 +2515,13 @@ static float *game_colours(frontend *fe, game_state *state, int *ncolours) ret[COL_BARRIER * 3 + 1] = 0.0F; ret[COL_BARRIER * 3 + 2] = 0.0F; + /* + * Highlighted loops are red as well. + */ + ret[COL_LOOP * 3 + 0] = 1.0F; + ret[COL_LOOP * 3 + 1] = 0.0F; + ret[COL_LOOP * 3 + 2] = 0.0F; + /* * Unpowered endpoints are blue. */ @@ -2063,17 +2546,17 @@ static float *game_colours(frontend *fe, game_state *state, int *ncolours) return ret; } -static void draw_thick_line(frontend *fe, int x1, int y1, int x2, int y2, - int colour) +static void draw_filled_line(drawing *dr, int x1, int y1, int x2, int y2, + int colour) { - draw_line(fe, x1-1, y1, x2-1, y2, COL_WIRE); - draw_line(fe, x1+1, y1, x2+1, y2, COL_WIRE); - draw_line(fe, x1, y1-1, x2, y2-1, COL_WIRE); - draw_line(fe, x1, y1+1, x2, y2+1, COL_WIRE); - draw_line(fe, x1, y1, x2, y2, colour); + draw_line(dr, x1-1, y1, x2-1, y2, COL_WIRE); + draw_line(dr, x1+1, y1, x2+1, y2, COL_WIRE); + draw_line(dr, x1, y1-1, x2, y2-1, COL_WIRE); + draw_line(dr, x1, y1+1, x2, y2+1, COL_WIRE); + draw_line(dr, x1, y1, x2, y2, colour); } -static void draw_rect_coords(frontend *fe, int x1, int y1, int x2, int y2, +static void draw_rect_coords(drawing *dr, int x1, int y1, int x2, int y2, int colour) { int mx = (x1 < x2 ? x1 : x2); @@ -2081,14 +2564,14 @@ static void draw_rect_coords(frontend *fe, int x1, int y1, int x2, int y2, int dx = (x2 + x1 - 2*mx + 1); int dy = (y2 + y1 - 2*my + 1); - draw_rect(fe, mx, my, dx, dy, colour); + draw_rect(dr, mx, my, dx, dy, colour); } /* * draw_barrier_corner() and draw_barrier() are passed physical coords */ -static void draw_barrier_corner(frontend *fe, int x, int y, int dx, int dy, - int phase) +static void draw_barrier_corner(drawing *dr, game_drawstate *ds, + int x, int y, int dx, int dy, int phase) { int bx = WINDOW_OFFSET + TILE_SIZE * x; int by = WINDOW_OFFSET + TILE_SIZE * y; @@ -2098,20 +2581,21 @@ static void draw_barrier_corner(frontend *fe, int x, int y, int dx, int dy, y1 = (dy > 0 ? TILE_SIZE+TILE_BORDER-1 : 0); if (phase == 0) { - draw_rect_coords(fe, bx+x1+dx, by+y1, + draw_rect_coords(dr, bx+x1+dx, by+y1, bx+x1-TILE_BORDER*dx, by+y1-(TILE_BORDER-1)*dy, COL_WIRE); - draw_rect_coords(fe, bx+x1, by+y1+dy, + draw_rect_coords(dr, bx+x1, by+y1+dy, bx+x1-(TILE_BORDER-1)*dx, by+y1-TILE_BORDER*dy, COL_WIRE); } else { - draw_rect_coords(fe, bx+x1, by+y1, + draw_rect_coords(dr, bx+x1, by+y1, bx+x1-(TILE_BORDER-1)*dx, by+y1-(TILE_BORDER-1)*dy, COL_BARRIER); } } -static void draw_barrier(frontend *fe, int x, int y, int dir, int phase) +static void draw_barrier(drawing *dr, game_drawstate *ds, + int x, int y, int dir, int phase) { int bx = WINDOW_OFFSET + TILE_SIZE * x; int by = WINDOW_OFFSET + TILE_SIZE * y; @@ -2123,16 +2607,16 @@ static void draw_barrier(frontend *fe, int x, int y, int dir, int phase) h = (Y(dir) ? TILE_BORDER : TILE_SIZE - TILE_BORDER); if (phase == 0) { - draw_rect(fe, bx+x1-X(dir), by+y1-Y(dir), w, h, COL_WIRE); + draw_rect(dr, bx+x1-X(dir), by+y1-Y(dir), w, h, COL_WIRE); } else { - draw_rect(fe, bx+x1, by+y1, w, h, COL_BARRIER); + draw_rect(dr, bx+x1, by+y1, w, h, COL_BARRIER); } } /* * draw_tile() is passed physical coordinates */ -static void draw_tile(frontend *fe, game_state *state, game_drawstate *ds, +static void draw_tile(drawing *dr, const game_state *state, game_drawstate *ds, int x, int y, int tile, int src, float angle, int cursor) { int bx = WINDOW_OFFSET + TILE_SIZE * x; @@ -2148,16 +2632,16 @@ static void draw_tile(frontend *fe, game_state *state, game_drawstate *ds, * we must draw those connections on the borders themselves. */ - clip(fe, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER); + clip(dr, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER); /* * So. First blank the tile out completely: draw a big * rectangle in border colour, and a smaller rectangle in * background colour to fill it in. */ - draw_rect(fe, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER, + draw_rect(dr, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER, COL_BORDER); - draw_rect(fe, bx+TILE_BORDER, by+TILE_BORDER, + draw_rect(dr, bx+TILE_BORDER, by+TILE_BORDER, TILE_SIZE-TILE_BORDER, TILE_SIZE-TILE_BORDER, tile & LOCKED ? COL_LOCKED : COL_BACKGROUND); @@ -2167,16 +2651,16 @@ static void draw_tile(frontend *fe, game_state *state, game_drawstate *ds, * in. */ if (cursor) { - draw_line(fe, bx+TILE_SIZE/8, by+TILE_SIZE/8, + draw_line(dr, bx+TILE_SIZE/8, by+TILE_SIZE/8, bx+TILE_SIZE/8, by+TILE_SIZE-TILE_SIZE/8, tile & LOCKED ? COL_BACKGROUND : COL_LOCKED); - draw_line(fe, bx+TILE_SIZE/8, by+TILE_SIZE/8, + draw_line(dr, bx+TILE_SIZE/8, by+TILE_SIZE/8, bx+TILE_SIZE-TILE_SIZE/8, by+TILE_SIZE/8, tile & LOCKED ? COL_BACKGROUND : COL_LOCKED); - draw_line(fe, bx+TILE_SIZE-TILE_SIZE/8, by+TILE_SIZE/8, + draw_line(dr, bx+TILE_SIZE-TILE_SIZE/8, by+TILE_SIZE/8, bx+TILE_SIZE-TILE_SIZE/8, by+TILE_SIZE-TILE_SIZE/8, tile & LOCKED ? COL_BACKGROUND : COL_LOCKED); - draw_line(fe, bx+TILE_SIZE/8, by+TILE_SIZE-TILE_SIZE/8, + draw_line(dr, bx+TILE_SIZE/8, by+TILE_SIZE-TILE_SIZE/8, bx+TILE_SIZE-TILE_SIZE/8, by+TILE_SIZE-TILE_SIZE/8, tile & LOCKED ? COL_BACKGROUND : COL_LOCKED); } @@ -2199,9 +2683,9 @@ static void draw_tile(frontend *fe, game_state *state, game_drawstate *ds, ex = (TILE_SIZE - TILE_BORDER - 1.0F) / 2.0F * X(dir); ey = (TILE_SIZE - TILE_BORDER - 1.0F) / 2.0F * Y(dir); MATMUL(tx, ty, matrix, ex, ey); - draw_thick_line(fe, bx+(int)cx, by+(int)cy, - bx+(int)(cx+tx), by+(int)(cy+ty), - COL_WIRE); + draw_filled_line(dr, bx+(int)cx, by+(int)cy, + bx+(int)(cx+tx), by+(int)(cy+ty), + COL_WIRE); } } for (dir = 1; dir < 0x10; dir <<= 1) { @@ -2209,10 +2693,15 @@ static void draw_tile(frontend *fe, game_state *state, game_drawstate *ds, ex = (TILE_SIZE - TILE_BORDER - 1.0F) / 2.0F * X(dir); ey = (TILE_SIZE - TILE_BORDER - 1.0F) / 2.0F * Y(dir); MATMUL(tx, ty, matrix, ex, ey); - draw_line(fe, bx+(int)cx, by+(int)cy, - bx+(int)(cx+tx), by+(int)(cy+ty), col); + draw_line(dr, bx+(int)cx, by+(int)cy, + bx+(int)(cx+tx), by+(int)(cy+ty), + (tile & LOOP(dir)) ? COL_LOOP : col); } } + /* If we've drawn any loop-highlighted arms, make sure the centre + * point is loop-coloured rather than a later arm overwriting it. */ + if (tile & (RLOOP | ULOOP | LLOOP | DLOOP)) + draw_rect(dr, bx+(int)cx, by+(int)cy, 1, 1, COL_LOOP); /* * Draw the box in the middle. We do this in blue if the tile @@ -2242,8 +2731,7 @@ static void draw_tile(frontend *fe, game_state *state, game_drawstate *ds, points[i+1] = by+(int)(cy+ty); } - draw_polygon(fe, points, 4, TRUE, col); - draw_polygon(fe, points, 4, FALSE, COL_WIRE); + draw_polygon(dr, points, 4, col, COL_WIRE); } /* @@ -2280,16 +2768,18 @@ static void draw_tile(frontend *fe, game_state *state, game_drawstate *ds, * in: if we are fully connected to the other tile then * the two ACTIVE states will be the same.) */ - draw_rect_coords(fe, px-vx, py-vy, px+lx+vx, py+ly+vy, COL_WIRE); - draw_rect_coords(fe, px, py, px+lx, py+ly, - (tile & ACTIVE) ? COL_POWERED : COL_WIRE); + draw_rect_coords(dr, px-vx, py-vy, px+lx+vx, py+ly+vy, COL_WIRE); + draw_rect_coords(dr, px, py, px+lx, py+ly, + ((tile & LOOP(dir)) ? COL_LOOP : + (tile & ACTIVE) ? COL_POWERED : + COL_WIRE)); } else { /* * The other tile extends into our border, but isn't * actually connected to us. Just draw a single black * dot. */ - draw_rect_coords(fe, px, py, px, py, COL_WIRE); + draw_rect_coords(dr, px, py, px, py, COL_WIRE); } } @@ -2333,7 +2823,7 @@ static void draw_tile(frontend *fe, game_state *state, game_drawstate *ds, * At least one barrier terminates here. Draw a * corner. */ - draw_barrier_corner(fe, x, y, + draw_barrier_corner(dr, ds, x, y, X(dir)+X(A(dir)), Y(dir)+Y(A(dir)), phase); } @@ -2341,19 +2831,22 @@ static void draw_tile(frontend *fe, game_state *state, game_drawstate *ds, for (dir = 1; dir < 0x10; dir <<= 1) if (barrier(state, GX(x), GY(y)) & dir) - draw_barrier(fe, x, y, dir, phase); + draw_barrier(dr, ds, x, y, dir, phase); } - unclip(fe); + unclip(dr); - draw_update(fe, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER); + draw_update(dr, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER); } -static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, - game_state *state, int dir, game_ui *ui, float t, float ft) +static void game_redraw(drawing *dr, game_drawstate *ds, + const game_state *oldstate, const game_state *state, + int dir, const game_ui *ui, + float t, float ft) { int x, y, tx, ty, frame, last_rotate_dir, moved_origin = FALSE; unsigned char *active; + int *loops; float angle = 0.0; /* @@ -2365,7 +2858,7 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, ds->started = TRUE; - draw_rect(fe, 0, 0, + draw_rect(dr, 0, 0, WINDOW_OFFSET * 2 + TILE_SIZE * state->width + TILE_BORDER, WINDOW_OFFSET * 2 + TILE_SIZE * state->height + TILE_BORDER, COL_BACKGROUND); @@ -2374,7 +2867,7 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, ds->org_y = ui->org_y; moved_origin = TRUE; - draw_update(fe, 0, 0, + draw_update(dr, 0, 0, WINDOW_OFFSET*2 + TILE_SIZE*state->width + TILE_BORDER, WINDOW_OFFSET*2 + TILE_SIZE*state->height + TILE_BORDER); @@ -2383,38 +2876,38 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, for (x = 0; x < ds->width; x++) { if (x+1 < ds->width) { if (barrier(state, GX(x), GY(0)) & R) - draw_barrier_corner(fe, x, -1, +1, +1, phase); + draw_barrier_corner(dr, ds, x, -1, +1, +1, phase); if (barrier(state, GX(x), GY(ds->height-1)) & R) - draw_barrier_corner(fe, x, ds->height, +1, -1, phase); + draw_barrier_corner(dr, ds, x, ds->height, +1, -1, phase); } if (barrier(state, GX(x), GY(0)) & U) { - draw_barrier_corner(fe, x, -1, -1, +1, phase); - draw_barrier_corner(fe, x, -1, +1, +1, phase); - draw_barrier(fe, x, -1, D, phase); + draw_barrier_corner(dr, ds, x, -1, -1, +1, phase); + draw_barrier_corner(dr, ds, x, -1, +1, +1, phase); + draw_barrier(dr, ds, x, -1, D, phase); } if (barrier(state, GX(x), GY(ds->height-1)) & D) { - draw_barrier_corner(fe, x, ds->height, -1, -1, phase); - draw_barrier_corner(fe, x, ds->height, +1, -1, phase); - draw_barrier(fe, x, ds->height, U, phase); + draw_barrier_corner(dr, ds, x, ds->height, -1, -1, phase); + draw_barrier_corner(dr, ds, x, ds->height, +1, -1, phase); + draw_barrier(dr, ds, x, ds->height, U, phase); } } for (y = 0; y < ds->height; y++) { if (y+1 < ds->height) { if (barrier(state, GX(0), GY(y)) & D) - draw_barrier_corner(fe, -1, y, +1, +1, phase); + draw_barrier_corner(dr, ds, -1, y, +1, +1, phase); if (barrier(state, GX(ds->width-1), GY(y)) & D) - draw_barrier_corner(fe, ds->width, y, -1, +1, phase); + draw_barrier_corner(dr, ds, ds->width, y, -1, +1, phase); } if (barrier(state, GX(0), GY(y)) & L) { - draw_barrier_corner(fe, -1, y, +1, -1, phase); - draw_barrier_corner(fe, -1, y, +1, +1, phase); - draw_barrier(fe, -1, y, R, phase); + draw_barrier_corner(dr, ds, -1, y, +1, -1, phase); + draw_barrier_corner(dr, ds, -1, y, +1, +1, phase); + draw_barrier(dr, ds, -1, y, R, phase); } if (barrier(state, GX(ds->width-1), GY(y)) & R) { - draw_barrier_corner(fe, ds->width, y, -1, -1, phase); - draw_barrier_corner(fe, ds->width, y, -1, +1, phase); - draw_barrier(fe, ds->width, y, L, phase); + draw_barrier_corner(dr, ds, ds->width, y, -1, -1, phase); + draw_barrier_corner(dr, ds, ds->width, y, -1, +1, phase); + draw_barrier(dr, ds, ds->width, y, L, phase); } } } @@ -2447,11 +2940,13 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, * Draw any tile which differs from the way it was last drawn. */ active = compute_active(state, ui->cx, ui->cy); + loops = compute_loops(state); for (x = 0; x < ds->width; x++) for (y = 0; y < ds->height; y++) { - unsigned char c = tile(state, GX(x), GY(y)) | - index(state, active, GX(x), GY(y)); + int c = tile(state, GX(x), GY(y)) | + index(state, active, GX(x), GY(y)) | + index(state, loops, GX(x), GY(y)); int is_src = GX(x) == ui->cx && GY(y) == ui->cy; int is_anim = GX(x) == tx && GY(y) == ty; int is_cursor = ui->cur_visible && @@ -2478,12 +2973,12 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, if (moved_origin || index(state, ds->visible, x, y) != c || - index(state, ds->visible, x, y) == 0xFF || + index(state, ds->visible, x, y) == -1 || is_src || is_anim || is_cursor) { - draw_tile(fe, state, ds, x, y, c, + draw_tile(dr, state, ds, x, y, c, is_src, (is_anim ? angle : 0.0F), is_cursor); if (is_src || is_anim || is_cursor) - index(state, ds->visible, x, y) = 0xFF; + index(state, ds->visible, x, y) = -1; else index(state, ds->visible, x, y) = c; } @@ -2493,39 +2988,56 @@ static void game_redraw(frontend *fe, game_drawstate *ds, game_state *oldstate, * Update the status bar. */ { - char statusbuf[256]; + char statusbuf[256], *p; int i, n, n2, a; + int complete = FALSE; + + p = statusbuf; + *p = '\0'; /* ensure even an empty status string is terminated */ - n = state->width * state->height; - for (i = a = n2 = 0; i < n; i++) { - if (active[i]) - a++; - if (state->tiles[i] & 0xF) - n2++; + if (state->used_solve) { + p += sprintf(p, "Auto-solved. "); + complete = TRUE; + } else if (state->completed) { + p += sprintf(p, "COMPLETED! "); + complete = TRUE; } - sprintf(statusbuf, "%sActive: %d/%d", - (state->used_solve ? "Auto-solved. " : - state->completed ? "COMPLETED! " : ""), a, n2); + /* + * Omit the 'Active: n/N' counter completely if the source + * tile is a completely empty one, because then the active + * count can't help but read '1'. + */ + if (tile(state, ui->cx, ui->cy) & 0xF) { + n = state->width * state->height; + for (i = a = n2 = 0; i < n; i++) { + if (active[i]) + a++; + if (state->tiles[i] & 0xF) + n2++; + } - status_bar(fe, statusbuf); + /* + * Also, if we're displaying a completion indicator and + * the game is still in its completed state (i.e. every + * tile is active), we might as well omit this too. + */ + if (!complete || a < n2) + p += sprintf(p, "Active: %d/%d", a, n2); + } + + status_bar(dr, statusbuf); } sfree(active); + sfree(loops); } -static float game_anim_length(game_state *oldstate, - game_state *newstate, int dir, game_ui *ui) +static float game_anim_length(const game_state *oldstate, + const game_state *newstate, int dir, game_ui *ui) { int last_rotate_dir; - /* - * Don't animate an auto-solve move. - */ - if ((dir > 0 && newstate->just_used_solve) || - (dir < 0 && oldstate->just_used_solve)) - return 0.0F; - /* * Don't animate if last_rotate_dir is zero. */ @@ -2537,8 +3049,8 @@ static float game_anim_length(game_state *oldstate, return 0.0F; } -static float game_flash_length(game_state *oldstate, - game_state *newstate, int dir, game_ui *ui) +static float game_flash_length(const game_state *oldstate, + const game_state *newstate, int dir, game_ui *ui) { /* * If the game has just been completed, we display a completion @@ -2557,24 +3069,159 @@ static float game_flash_length(game_state *oldstate, return 0.0F; } -static int game_wants_statusbar(void) +static int game_status(const game_state *state) { - return TRUE; + return state->completed ? +1 : 0; } -static int game_timing_state(game_state *state) +static int game_timing_state(const game_state *state, game_ui *ui) { return TRUE; } +static void game_print_size(const game_params *params, float *x, float *y) +{ + int pw, ph; + + /* + * I'll use 8mm squares by default. + */ + game_compute_size(params, 800, &pw, &ph); + *x = pw / 100.0F; + *y = ph / 100.0F; +} + +static void draw_diagram(drawing *dr, game_drawstate *ds, int x, int y, + int topleft, int v, int drawlines, int ink) +{ + int tx, ty, cx, cy, r, br, k, thick; + + tx = WINDOW_OFFSET + TILE_SIZE * x; + ty = WINDOW_OFFSET + TILE_SIZE * y; + + /* + * Find our centre point. + */ + if (topleft) { + cx = tx + (v & L ? TILE_SIZE / 4 : TILE_SIZE / 6); + cy = ty + (v & U ? TILE_SIZE / 4 : TILE_SIZE / 6); + r = TILE_SIZE / 8; + br = TILE_SIZE / 32; + } else { + cx = tx + TILE_SIZE / 2; + cy = ty + TILE_SIZE / 2; + r = TILE_SIZE / 2; + br = TILE_SIZE / 8; + } + thick = r / 20; + + /* + * Draw the square block if we have an endpoint. + */ + if (v == 1 || v == 2 || v == 4 || v == 8) + draw_rect(dr, cx - br, cy - br, br*2, br*2, ink); + + /* + * Draw each radial line. + */ + if (drawlines) { + for (k = 1; k < 16; k *= 2) + if (v & k) { + int x1 = min(cx, cx + (r-thick) * X(k)); + int x2 = max(cx, cx + (r-thick) * X(k)); + int y1 = min(cy, cy + (r-thick) * Y(k)); + int y2 = max(cy, cy + (r-thick) * Y(k)); + draw_rect(dr, x1 - thick, y1 - thick, + (x2 - x1) + 2*thick, (y2 - y1) + 2*thick, ink); + } + } +} + +static void game_print(drawing *dr, const game_state *state, int tilesize) +{ + int w = state->width, h = state->height; + int ink = print_mono_colour(dr, 0); + int x, y; + + /* Ick: fake up `ds->tilesize' for macro expansion purposes */ + game_drawstate ads, *ds = &ads; + game_set_size(dr, ds, NULL, tilesize); + + /* + * Border. + */ + print_line_width(dr, TILE_SIZE / (state->wrapping ? 128 : 12)); + draw_rect_outline(dr, WINDOW_OFFSET, WINDOW_OFFSET, + TILE_SIZE * w, TILE_SIZE * h, ink); + + /* + * Grid. + */ + print_line_width(dr, TILE_SIZE / 128); + for (x = 1; x < w; x++) + draw_line(dr, WINDOW_OFFSET + TILE_SIZE * x, WINDOW_OFFSET, + WINDOW_OFFSET + TILE_SIZE * x, WINDOW_OFFSET + TILE_SIZE * h, + ink); + for (y = 1; y < h; y++) + draw_line(dr, WINDOW_OFFSET, WINDOW_OFFSET + TILE_SIZE * y, + WINDOW_OFFSET + TILE_SIZE * w, WINDOW_OFFSET + TILE_SIZE * y, + ink); + + /* + * Barriers. + */ + for (y = 0; y <= h; y++) + for (x = 0; x <= w; x++) { + int b = barrier(state, x % w, y % h); + if (x < w && (b & U)) + draw_rect(dr, WINDOW_OFFSET + TILE_SIZE * x - TILE_SIZE/24, + WINDOW_OFFSET + TILE_SIZE * y - TILE_SIZE/24, + TILE_SIZE + TILE_SIZE/24 * 2, TILE_SIZE/24 * 2, ink); + if (y < h && (b & L)) + draw_rect(dr, WINDOW_OFFSET + TILE_SIZE * x - TILE_SIZE/24, + WINDOW_OFFSET + TILE_SIZE * y - TILE_SIZE/24, + TILE_SIZE/24 * 2, TILE_SIZE + TILE_SIZE/24 * 2, ink); + } + + /* + * Grid contents. + */ + for (y = 0; y < h; y++) + for (x = 0; x < w; x++) { + int vx, v = tile(state, x, y); + int locked = v & LOCKED; + + v &= 0xF; + + /* + * Rotate into a standard orientation for the top left + * corner diagram. + */ + vx = v; + while (vx != 0 && vx != 15 && vx != 1 && vx != 9 && vx != 13 && + vx != 5) + vx = A(vx); + + /* + * Draw the top left corner diagram. + */ + draw_diagram(dr, ds, x, y, TRUE, vx, TRUE, ink); + + /* + * Draw the real solution diagram, if we're doing so. + */ + draw_diagram(dr, ds, x, y, FALSE, v, locked, ink); + } +} + #ifdef COMBINED #define thegame net #endif const struct game thegame = { - "Net", "games.net", + "Net", "games.net", "net", default_params, - game_fetch_preset, + game_fetch_preset, NULL, decode_params, encode_params, free_params, @@ -2582,24 +3229,29 @@ const struct game thegame = { 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, - FALSE, game_text_format, + FALSE, game_can_format_as_text_now, game_text_format, new_ui, free_ui, - make_move, - game_size, + encode_ui, + decode_ui, + game_changed_state, + interpret_move, + execute_move, + PREFERRED_TILE_SIZE, game_compute_size, game_set_size, game_colours, game_new_drawstate, game_free_drawstate, game_redraw, game_anim_length, game_flash_length, - game_wants_statusbar, + game_status, + TRUE, FALSE, game_print_size, game_print, + TRUE, /* wants_statusbar */ FALSE, game_timing_state, - 0, /* mouse_priorities */ + 0, /* flags */ };