X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=blobdiff_plain;f=net.c;h=f4a1e39b451981844c66ab365e83aff90cdb4fb3;hb=9e62c710dfba0596eb355b95c46fd5153f5fe13d;hp=3e49b38651e7213c2815966ef7a33c908d9e1717;hpb=23ab000b7b038b6810e48a71a94b8287c20a6047;p=sgt-puzzles.git diff --git a/net.c b/net.c index 3e49b38..f4a1e39 100644 --- a/net.c +++ b/net.c @@ -12,12 +12,20 @@ #include "puzzles.h" #include "tree234.h" -#define MATMUL(xr,yr,m,x,y) do { \ - float rx, ry, xx = (x), yy = (y), *mat = (m); \ - rx = mat[0] * xx + mat[2] * yy; \ - ry = mat[1] * xx + mat[3] * yy; \ - (xr) = rx; (yr) = ry; \ -} while (0) +/* + * 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 /* Direction and other bitfields */ #define R 0x01 @@ -26,6 +34,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) ) @@ -45,19 +58,16 @@ #define PREFERRED_TILE_SIZE 32 #define TILE_SIZE (ds->tilesize) -#define TILE_BORDER 1 +#define LINE_THICK ((TILE_SIZE+47)/48) +#ifdef SMALL_SCREEN +#define WINDOW_OFFSET 4 +#else #define WINDOW_OFFSET 16 +#endif #define ROTATE_TIME 0.13F #define FLASH_FRAME 0.07F -/* Transform physical coords to game coords using game_drawstate ds */ -#define GX(x) (((x) + ds->org_x) % ds->width) -#define GY(y) (((y) + ds->org_y) % ds->height) -/* ...and game coords to physical coords */ -#define RX(x) (((x) + ds->width - ds->org_x) % ds->width) -#define RY(y) (((y) + ds->height - ds->org_y) % ds->height) - enum { COL_BACKGROUND, COL_LOCKED, @@ -66,6 +76,7 @@ enum { COL_ENDPOINT, COL_POWERED, COL_BARRIER, + COL_LOOP, NCOLOURS }; @@ -77,12 +88,17 @@ struct game_params { float barrier_probability; }; +typedef struct game_immutable_state { + int refcount; + unsigned char *barriers; +} game_immutable_state; + 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; + struct game_immutable_state *imm; }; #define OFFSETWH(x2,y2,x1,y1,dir,width,height) \ @@ -94,7 +110,7 @@ struct game_state { #define index(state, a, x, y) ( a[(y) * (state)->width + (x)] ) #define tile(state, x, y) index(state, (state)->tiles, x, y) -#define barrier(state, x, y) index(state, (state)->barriers, x, y) +#define barrier(state, x, y) index(state, (state)->imm->barriers, x, y) struct xyd { int x, y, direction; @@ -150,12 +166,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) @@ -182,7 +202,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 */ @@ -209,7 +229,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++; @@ -219,7 +239,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; @@ -237,7 +257,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]; @@ -280,7 +300,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); @@ -293,7 +313,7 @@ static game_params *custom_params(config_item *cfg) return ret; } -static char *validate_params(game_params *params, int full) +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"; @@ -430,6 +450,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) { @@ -521,9 +546,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 @@ -706,7 +729,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; @@ -786,14 +813,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; } } @@ -929,8 +956,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 @@ -1103,7 +1132,11 @@ 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, +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; @@ -1301,7 +1334,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; /* @@ -1382,13 +1415,98 @@ static char *new_game_desc(game_params *params, random_state *rs, /* * 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; } /* @@ -1480,7 +1598,7 @@ static char *new_game_desc(game_params *params, random_state *rs, return desc; } -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; @@ -1510,7 +1628,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 *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; @@ -1525,12 +1644,14 @@ static game_state *new_game(midend *me, game_params *params, char *desc) w = state->width = params->width; h = state->height = params->height; state->wrapping = params->wrapping; + state->imm = snew(game_immutable_state); + state->imm->refcount = 1; 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); - memset(state->barriers, 0, state->width * state->height); + state->imm->barriers = snewn(state->width * state->height, unsigned char); + memset(state->imm->barriers, 0, state->width * state->height); /* * Parse the game description into the grid. @@ -1587,7 +1708,7 @@ static game_state *new_game(midend *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; @@ -1596,37 +1717,39 @@ static game_state *new_game(midend *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; ret = snew(game_state); + ret->imm = state->imm; + ret->imm->refcount++; ret->width = state->width; ret->height = state->height; 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; ret->tiles = snewn(state->width * state->height, unsigned char); memcpy(ret->tiles, state->tiles, state->width * state->height); - ret->barriers = snewn(state->width * state->height, unsigned char); - memcpy(ret->barriers, state->barriers, state->width * state->height); return ret; } static void free_game(game_state *state) { + if (--state->imm->refcount == 0) { + sfree(state->imm->barriers); + sfree(state->imm); + } sfree(state->tiles); - sfree(state->barriers); sfree(state); } -static char *solve_game(game_state *state, game_state *currstate, - char *aux, char **error) +static char *solve_game(const game_state *state, const game_state *currstate, + const char *aux, char **error) { unsigned char *tiles; char *ret; @@ -1640,9 +1763,17 @@ static char *solve_game(game_state *state, game_state *currstate, * Run the internal solver on the provided grid. This might * not yield a complete solution. */ + int solver_result; + memcpy(tiles, state->tiles, state->width * state->height); - net_solver(state->width, state->height, tiles, - state->barriers, state->wrapping); + solver_result = net_solver(state->width, state->height, tiles, + state->imm->barriers, state->wrapping); + + if (solver_result < 0) { + *error = "No solution exists for this puzzle"; + sfree(tiles); + return NULL; + } } else { for (i = 0; i < state->width * state->height; i++) { int c = aux[i]; @@ -1720,7 +1851,12 @@ static char *solve_game(game_state *state, game_state *currstate, 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; } @@ -1736,7 +1872,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; @@ -1786,15 +1922,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->imm->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; @@ -1816,7 +2036,7 @@ static void free_ui(game_ui *ui) sfree(ui); } -static char *encode_ui(game_ui *ui) +static char *encode_ui(const game_ui *ui) { char buf[120]; /* @@ -1827,30 +2047,30 @@ static char *encode_ui(game_ui *ui) return dupstr(buf); } -static void decode_ui(game_ui *ui, char *encoding) +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, game_state *oldstate, - game_state *newstate) +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; - unsigned char *visible; + unsigned long *visible, *to_draw; }; /* ---------------------------------------------------------------------- * Process a move. */ -static char *interpret_move(game_state *state, game_ui *ui, - game_drawstate *ds, int x, int y, int button) +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; @@ -1866,6 +2086,12 @@ static char *interpret_move(game_state *state, game_ui *ui, 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) { @@ -1876,8 +2102,8 @@ static char *interpret_move(game_state *state, game_ui *ui, /* * The button must have been clicked on a valid tile. */ - x -= WINDOW_OFFSET + TILE_BORDER; - y -= WINDOW_OFFSET + TILE_BORDER; + x -= WINDOW_OFFSET + LINE_THICK; + y -= WINDOW_OFFSET + LINE_THICK; if (x < 0 || y < 0) return nullret; tx = x / TILE_SIZE; @@ -1887,14 +2113,117 @@ static char *interpret_move(game_state *state, game_ui *ui, /* Transform from physical to game coords */ tx = (tx + ui->org_x) % state->width; ty = (ty + ui->org_y) % state->height; - if (x % TILE_SIZE >= TILE_SIZE - TILE_BORDER || - y % TILE_SIZE >= TILE_SIZE - TILE_BORDER) + if (x % TILE_SIZE >= TILE_SIZE - LINE_THICK || + y % TILE_SIZE >= TILE_SIZE - LINE_THICK) return nullret; - action = button == LEFT_BUTTON ? ROTATE_LEFT : - button == RIGHT_BUTTON ? ROTATE_RIGHT : TOGGLE_LOCK; - } else if (button == CURSOR_UP || button == CURSOR_DOWN || - button == CURSOR_RIGHT || button == CURSOR_LEFT) { +#ifdef USE_DRAGGING + + if (button == MIDDLE_BUTTON +#ifdef STYLUS_BASED + || button == RIGHT_BUTTON /* with a stylus, `right-click' locks */ +#endif + ) { + /* + * Middle button never drags: it only toggles the lock. + */ + action = TOGGLE_LOCK; + } else if (button == LEFT_BUTTON +#ifndef STYLUS_BASED + || button == RIGHT_BUTTON /* (see above) */ +#endif + ) { + /* + * Otherwise, we note down the start point for a drag. + */ + 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 + ) { + /* + * Find the new drag point and see if it necessitates a + * rotation. + */ + 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 */ + } + +#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; @@ -1909,12 +2238,12 @@ static char *interpret_move(game_state *state, game_ui *ui, } else if (button == 'a' || button == 's' || button == 'd' || button == 'A' || button == 'S' || button == 'D' || button == 'f' || button == 'F' || - button == CURSOR_SELECT) { + IS_CURSOR_SELECT(button)) { tx = ui->cur_x; ty = ui->cur_y; if (button == 'a' || button == 'A' || button == CURSOR_SELECT) action = ROTATE_LEFT; - else if (button == 's' || button == 'S') + else if (button == 's' || button == 'S' || button == CURSOR_SELECT2) action = TOGGLE_LOCK; else if (button == 'd' || button == 'D') action = ROTATE_RIGHT; @@ -2013,17 +2342,16 @@ static char *interpret_move(game_state *state, game_ui *ui, } } -static game_state *execute_move(game_state *from, char *move) +static game_state *execute_move(const game_state *from, const char *move) { game_state *ret; - int tx, ty, n, noanim, orig; + int tx = -1, ty = -1, n, noanim, orig; ret = dup_game(from); - ret->just_used_solve = FALSE; if (move[0] == 'J' || move[0] == 'S') { if (move[0] == 'S') - ret->just_used_solve = ret->used_solve = TRUE; + ret->used_solve = TRUE; move++; if (*move == ';') @@ -2066,6 +2394,7 @@ static game_state *execute_move(game_state *from, char *move) } } if (!noanim) { + if (tx == -1 || ty == -1) { free_game(ret); return NULL; } ret->last_rotate_x = tx; ret->last_rotate_y = ty; } @@ -2073,24 +2402,31 @@ static game_state *execute_move(game_state *from, char *move) /* * 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 and correctly - * determine whether the game is 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, 0, 0); - 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; @@ -2104,41 +2440,52 @@ static game_state *execute_move(game_state *from, char *move) * Routines for drawing the game position on the screen. */ -static game_drawstate *game_new_drawstate(drawing *dr, game_state *state) +static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state) { game_drawstate *ds = snew(game_drawstate); + int i, ncells; 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); + ncells = (state->width+2) * (state->height+2); + ds->visible = snewn(ncells, unsigned long); + ds->to_draw = snewn(ncells, unsigned long); ds->tilesize = 0; /* undecided yet */ - memset(ds->visible, 0xFF, state->width * state->height); + for (i = 0; i < ncells; i++) + ds->visible[i] = -1; return ds; } +#define dsindex(ds, field, x, y) ((ds)->field[((y)+1)*((ds)->width+2)+((x)+1)]) +#define visible(ds, x, y) dsindex(ds, visible, x, y) +#define todraw(ds, x, y) dsindex(ds, to_draw, x, y) + static void game_free_drawstate(drawing *dr, game_drawstate *ds) { sfree(ds->visible); sfree(ds); } -static void game_compute_size(game_params *params, int tilesize, - 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; + /* Ick: fake up `ds->tilesize' for macro expansion purposes */ + struct { int tilesize; } ads, *ds = &ads; + ads.tilesize = tilesize; + + *x = WINDOW_OFFSET * 2 + TILE_SIZE * params->width + LINE_THICK; + *y = WINDOW_OFFSET * 2 + TILE_SIZE * params->height + LINE_THICK; } static void game_set_size(drawing *dr, game_drawstate *ds, - game_params *params, int tilesize) + const game_params *params, int tilesize) { ds->tilesize = tilesize; } -static float *game_colours(frontend *fe, game_state *state, int *ncolours) +static float *game_colours(frontend *fe, int *ncolours) { float *ret; @@ -2172,6 +2519,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. */ @@ -2196,361 +2550,313 @@ static float *game_colours(frontend *fe, game_state *state, int *ncolours) return ret; } -static void draw_thick_line(drawing *dr, int x1, int y1, int x2, int y2, - int colour) +static void rotated_coords(float *ox, float *oy, const float matrix[4], + float cx, float cy, float ix, float iy) { - 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); + *ox = matrix[0] * ix + matrix[2] * iy + cx; + *oy = matrix[1] * ix + matrix[3] * iy + cy; } -static void draw_rect_coords(drawing *dr, int x1, int y1, int x2, int y2, - int colour) +/* Flags describing the visible features of a tile. */ +#define TILE_BARRIER_SHIFT 0 /* 4 bits: R U L D */ +#define TILE_BARRIER_CORNER_SHIFT 4 /* 4 bits: RU UL LD DR */ +#define TILE_KEYBOARD_CURSOR (1<<8) /* 1 bit if cursor is here */ +#define TILE_WIRE_SHIFT 9 /* 8 bits: RR UU LL DD + * Each pair: 0=no wire, 1=unpowered, + * 2=powered, 3=loop err highlight */ +#define TILE_ENDPOINT_SHIFT 17 /* 2 bits: 0=no endpoint, 1=unpowered, + * 2=powered, 3=power-source square */ +#define TILE_WIRE_ON_EDGE_SHIFT 19 /* 8 bits: RR UU LL DD, + * same encoding as TILE_WIRE_SHIFT */ +#define TILE_ROTATING (1UL<<27) /* 1 bit if tile is rotating */ +#define TILE_LOCKED (1UL<<28) /* 1 bit if tile is locked */ + +static void draw_wires(drawing *dr, int cx, int cy, int radius, + unsigned long tile, int bitmap, + int colour, int halfwidth, const float matrix[4]) { - int mx = (x1 < x2 ? x1 : x2); - int my = (y1 < y2 ? y1 : y2); - int dx = (x2 + x1 - 2*mx + 1); - int dy = (y2 + y1 - 2*my + 1); - - draw_rect(dr, mx, my, dx, dy, colour); -} - -/* - * draw_barrier_corner() and draw_barrier() are passed physical coords - */ -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; - int x1, y1; - - x1 = (dx > 0 ? TILE_SIZE+TILE_BORDER-1 : 0); - y1 = (dy > 0 ? TILE_SIZE+TILE_BORDER-1 : 0); - - if (phase == 0) { - 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(dr, bx+x1, by+y1+dy, - bx+x1-(TILE_BORDER-1)*dx, by+y1-TILE_BORDER*dy, - COL_WIRE); - } else { - draw_rect_coords(dr, bx+x1, by+y1, - bx+x1-(TILE_BORDER-1)*dx, by+y1-(TILE_BORDER-1)*dy, - COL_BARRIER); + float fpoints[12*2]; + int points[12*2]; + int npoints, d, dsh, i; + int any_wire_this_colour = FALSE; + float xf, yf; + + npoints = 0; + for (d = 1, dsh = 0; d < 16; d *= 2, dsh++) { + int wiretype = (tile >> (TILE_WIRE_SHIFT + 2*dsh)) & 3; + + fpoints[2*npoints+0] = halfwidth * (X(d) + X(C(d))); + fpoints[2*npoints+1] = halfwidth * (Y(d) + Y(C(d))); + npoints++; + + if (bitmap & (1 << wiretype)) { + fpoints[2*npoints+0] = radius * X(d) + halfwidth * X(C(d)); + fpoints[2*npoints+1] = radius * Y(d) + halfwidth * Y(C(d)); + npoints++; + fpoints[2*npoints+0] = radius * X(d) + halfwidth * X(A(d)); + fpoints[2*npoints+1] = radius * Y(d) + halfwidth * Y(A(d)); + npoints++; + + any_wire_this_colour = TRUE; + } } -} - -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; - int x1, y1, w, h; - x1 = (X(dir) > 0 ? TILE_SIZE : X(dir) == 0 ? TILE_BORDER : 0); - y1 = (Y(dir) > 0 ? TILE_SIZE : Y(dir) == 0 ? TILE_BORDER : 0); - w = (X(dir) ? TILE_BORDER : TILE_SIZE - TILE_BORDER); - h = (Y(dir) ? TILE_BORDER : TILE_SIZE - TILE_BORDER); + if (!any_wire_this_colour) + return; - if (phase == 0) { - draw_rect(dr, bx+x1-X(dir), by+y1-Y(dir), w, h, COL_WIRE); - } else { - draw_rect(dr, bx+x1, by+y1, w, h, COL_BARRIER); + for (i = 0; i < npoints; i++) { + rotated_coords(&xf, &yf, matrix, cx, cy, fpoints[2*i], fpoints[2*i+1]); + points[2*i] = 0.5 + xf; + points[2*i+1] = 0.5 + yf; } + + draw_polygon(dr, points, npoints, colour, colour); } -/* - * draw_tile() is passed physical coordinates - */ -static void draw_tile(drawing *dr, game_state *state, game_drawstate *ds, - int x, int y, int tile, int src, float angle, int cursor) +static void draw_tile(drawing *dr, game_drawstate *ds, int x, int y, + unsigned long tile, float angle) { - int bx = WINDOW_OFFSET + TILE_SIZE * x; - int by = WINDOW_OFFSET + TILE_SIZE * y; + int tx, ty; + int clipx, clipy, clipX, clipY, clipw, cliph; + int border_br = LINE_THICK/2, border_tl = LINE_THICK - border_br; + int barrier_outline_thick = (LINE_THICK+1)/2; + int bg, d, dsh, pass; + int cx, cy, radius; float matrix[4]; - float cx, cy, ex, ey, tx, ty; - int dir, col, phase; + + tx = WINDOW_OFFSET + TILE_SIZE * x + border_br; + ty = WINDOW_OFFSET + TILE_SIZE * y + border_br; /* - * When we draw a single tile, we must draw everything up to - * and including the borders around the tile. This means that - * if the neighbouring tiles have connections to those borders, - * we must draw those connections on the borders themselves. + * Clip to the tile boundary, with adjustments if we're drawing + * just outside the grid. */ - - clip(dr, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER); + clipx = tx; clipX = tx + TILE_SIZE; + clipy = ty; clipY = ty + TILE_SIZE; + if (x == -1) { + clipx = clipX - border_br - barrier_outline_thick; + } else if (x == ds->width) { + clipX = clipx + border_tl + barrier_outline_thick; + } + if (y == -1) { + clipy = clipY - border_br - barrier_outline_thick; + } else if (y == ds->height) { + clipY = clipy + border_tl + barrier_outline_thick; + } + clipw = clipX - clipx; + cliph = clipY - clipy; + clip(dr, clipx, clipy, clipw, cliph); /* - * 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. + * Clear the clip region. */ - draw_rect(dr, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER, - COL_BORDER); - draw_rect(dr, bx+TILE_BORDER, by+TILE_BORDER, - TILE_SIZE-TILE_BORDER, TILE_SIZE-TILE_BORDER, - tile & LOCKED ? COL_LOCKED : COL_BACKGROUND); + bg = (tile & TILE_LOCKED) ? COL_LOCKED : COL_BACKGROUND; + draw_rect(dr, clipx, clipy, clipw, cliph, bg); /* - * Draw an inset outline rectangle as a cursor, in whichever of - * COL_LOCKED and COL_BACKGROUND we aren't currently drawing - * in. + * Draw the grid lines. */ - if (cursor) { - 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(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(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(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); + { + int gridl = (x == -1 ? tx+TILE_SIZE-border_br : tx); + int gridr = (x == ds->width ? tx+border_tl : tx+TILE_SIZE); + int gridu = (y == -1 ? ty+TILE_SIZE-border_br : ty); + int gridd = (y == ds->height ? ty+border_tl : ty+TILE_SIZE); + if (x >= 0) + draw_rect(dr, tx, gridu, border_tl, gridd-gridu, COL_BORDER); + if (y >= 0) + draw_rect(dr, gridl, ty, gridr-gridl, border_tl, COL_BORDER); + if (x < ds->width) + draw_rect(dr, tx+TILE_SIZE-border_br, gridu, + border_br, gridd-gridu, COL_BORDER); + if (y < ds->height) + draw_rect(dr, gridl, ty+TILE_SIZE-border_br, + gridr-gridl, border_br, COL_BORDER); } /* - * Set up the rotation matrix. + * Draw the keyboard cursor. */ - matrix[0] = (float)cos(angle * PI / 180.0); - matrix[1] = (float)-sin(angle * PI / 180.0); - matrix[2] = (float)sin(angle * PI / 180.0); - matrix[3] = (float)cos(angle * PI / 180.0); + if (tile & TILE_KEYBOARD_CURSOR) { + int cursorcol = (tile & TILE_LOCKED) ? COL_BACKGROUND : COL_LOCKED; + int inset_outer = TILE_SIZE/8, inset_inner = inset_outer + LINE_THICK; + draw_rect(dr, tx + inset_outer, ty + inset_outer, + TILE_SIZE - 2*inset_outer, TILE_SIZE - 2*inset_outer, + cursorcol); + draw_rect(dr, tx + inset_inner, ty + inset_inner, + TILE_SIZE - 2*inset_inner, TILE_SIZE - 2*inset_inner, + bg); + } + + radius = (TILE_SIZE+1)/2; + cx = tx + radius; + cy = ty + radius; + radius++; /* - * Draw the wires. + * Draw protrusions into this cell's edges of wires in + * neighbouring cells, as given by the TILE_WIRE_ON_EDGE_SHIFT + * flags. We only draw each of these if there _isn't_ a wire of + * our own that's going to overlap it, which means either the + * corresponding TILE_WIRE_SHIFT flag is zero, or else the + * TILE_ROTATING flag is set (so that our main wire won't be drawn + * in quite that place anyway). */ - cx = cy = TILE_BORDER + (TILE_SIZE-TILE_BORDER) / 2.0F - 0.5F; - col = (tile & ACTIVE ? COL_POWERED : COL_WIRE); - for (dir = 1; dir < 0x10; dir <<= 1) { - if (tile & dir) { - 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(dr, bx+(int)cx, by+(int)cy, - bx+(int)(cx+tx), by+(int)(cy+ty), - COL_WIRE); - } - } - for (dir = 1; dir < 0x10; dir <<= 1) { - if (tile & dir) { - 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(dr, bx+(int)cx, by+(int)cy, - bx+(int)(cx+tx), by+(int)(cy+ty), col); + for (d = 1, dsh = 0; d < 16; d *= 2, dsh++) { + int edgetype = ((tile >> (TILE_WIRE_ON_EDGE_SHIFT + 2*dsh)) & 3); + if (edgetype == 0) + continue; /* there isn't a wire on the edge */ + if (!(tile & TILE_ROTATING) && + ((tile >> (TILE_WIRE_SHIFT + 2*dsh)) & 3) != 0) + continue; /* wire on edge would be overdrawn anyway */ + + for (pass = 0; pass < 2; pass++) { + int x, y, w, h; + int col = (pass == 0 || edgetype == 1 ? COL_WIRE : + edgetype == 2 ? COL_POWERED : COL_LOOP); + int halfwidth = pass == 0 ? 2*LINE_THICK-1 : LINE_THICK-1; + + if (X(d) < 0) { + x = tx; + w = border_tl; + } else if (X(d) > 0) { + x = tx + TILE_SIZE - border_br; + w = border_br; + } else { + x = cx - halfwidth; + w = 2 * halfwidth + 1; + } + + if (Y(d) < 0) { + y = ty; + h = border_tl; + } else if (Y(d) > 0) { + y = ty + TILE_SIZE - border_br; + h = border_br; + } else { + y = cy - halfwidth; + h = 2 * halfwidth + 1; + } + + draw_rect(dr, x, y, w, h, col); } } /* - * Draw the box in the middle. We do this in blue if the tile - * is an unpowered endpoint, in cyan if the tile is a powered - * endpoint, in black if the tile is the centrepiece, and - * otherwise not at all. + * Set up the rotation matrix for the main cell contents, i.e. + * everything that is centred in the grid square and optionally + * rotated by an arbitrary angle about that centre point. */ - col = -1; - if (src) - col = COL_WIRE; - else if (COUNT(tile) == 1) { - col = (tile & ACTIVE ? COL_POWERED : COL_ENDPOINT); - } - if (col >= 0) { - int i, points[8]; - - points[0] = +1; points[1] = +1; - points[2] = +1; points[3] = -1; - points[4] = -1; points[5] = -1; - points[6] = -1; points[7] = +1; - - for (i = 0; i < 8; i += 2) { - ex = (TILE_SIZE * 0.24F) * points[i]; - ey = (TILE_SIZE * 0.24F) * points[i+1]; - MATMUL(tx, ty, matrix, ex, ey); - points[i] = bx+(int)(cx+tx); - points[i+1] = by+(int)(cy+ty); - } - - draw_polygon(dr, points, 4, col, COL_WIRE); + if (tile & TILE_ROTATING) { + matrix[0] = (float)cos(angle * PI / 180.0); + matrix[2] = (float)sin(angle * PI / 180.0); + } else { + matrix[0] = 1.0F; + matrix[2] = 0.0F; } + matrix[3] = matrix[0]; + matrix[1] = -matrix[2]; /* - * Draw the points on the border if other tiles are connected - * to us. + * Draw the wires. */ - for (dir = 1; dir < 0x10; dir <<= 1) { - int dx, dy, px, py, lx, ly, vx, vy, ox, oy; - - dx = X(dir); - dy = Y(dir); - - ox = x + dx; - oy = y + dy; - - if (ox < 0 || ox >= state->width || oy < 0 || oy >= state->height) - continue; - - if (!(tile(state, GX(ox), GY(oy)) & F(dir))) - continue; + draw_wires(dr, cx, cy, radius, tile, + 0xE, COL_WIRE, 2*LINE_THICK-1, matrix); + draw_wires(dr, cx, cy, radius, tile, + 0x4, COL_POWERED, LINE_THICK-1, matrix); + draw_wires(dr, cx, cy, radius, tile, + 0x8, COL_LOOP, LINE_THICK-1, matrix); - px = bx + (int)(dx>0 ? TILE_SIZE + TILE_BORDER - 1 : dx<0 ? 0 : cx); - py = by + (int)(dy>0 ? TILE_SIZE + TILE_BORDER - 1 : dy<0 ? 0 : cy); - lx = dx * (TILE_BORDER-1); - ly = dy * (TILE_BORDER-1); - vx = (dy ? 1 : 0); - vy = (dx ? 1 : 0); + /* + * Draw the central box. + */ + for (pass = 0; pass < 2; pass++) { + int endtype = (tile >> TILE_ENDPOINT_SHIFT) & 3; + if (endtype) { + int i, points[8], col; + float boxr = TILE_SIZE * 0.24F + (pass == 0 ? LINE_THICK-1 : 0); + + col = (pass == 0 || endtype == 3 ? COL_WIRE : + endtype == 2 ? COL_POWERED : COL_ENDPOINT); + + points[0] = +1; points[1] = +1; + points[2] = +1; points[3] = -1; + points[4] = -1; points[5] = -1; + points[6] = -1; points[7] = +1; + + for (i = 0; i < 8; i += 2) { + float x, y; + rotated_coords(&x, &y, matrix, cx, cy, + boxr * points[i], boxr * points[i+1]); + points[i] = x + 0.5; + points[i+1] = y + 0.5; + } - if (angle == 0.0 && (tile & dir)) { - /* - * If we are fully connected to the other tile, we must - * draw right across the tile border. (We can use our - * own ACTIVE state to determine what colour to do this - * in: if we are fully connected to the other tile then - * the two ACTIVE states will be the same.) - */ - 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 & 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(dr, px, py, px, py, COL_WIRE); + draw_polygon(dr, points, 4, col, COL_WIRE); } } /* - * Draw barrier corners, and then barriers. + * Draw barriers along grid edges. */ - for (phase = 0; phase < 2; phase++) { - for (dir = 1; dir < 0x10; dir <<= 1) { - int x1, y1, corner = FALSE; - /* - * If at least one barrier terminates at the corner - * between dir and A(dir), draw a barrier corner. - */ - if (barrier(state, GX(x), GY(y)) & (dir | A(dir))) { - corner = TRUE; - } else { - /* - * Only count barriers terminating at this corner - * if they're physically next to the corner. (That - * is, if they've wrapped round from the far side - * of the screen, they don't count.) - */ - x1 = x + X(dir); - y1 = y + Y(dir); - if (x1 >= 0 && x1 < state->width && - y1 >= 0 && y1 < state->height && - (barrier(state, GX(x1), GY(y1)) & A(dir))) { - corner = TRUE; - } else { - x1 = x + X(A(dir)); - y1 = y + Y(A(dir)); - if (x1 >= 0 && x1 < state->width && - y1 >= 0 && y1 < state->height && - (barrier(state, GX(x1), GY(y1)) & dir)) - corner = TRUE; - } - } - - if (corner) { - /* - * At least one barrier terminates here. Draw a - * corner. - */ - draw_barrier_corner(dr, ds, x, y, - X(dir)+X(A(dir)), Y(dir)+Y(A(dir)), - phase); - } + for (pass = 0; pass < 2; pass++) { + int btl = border_tl, bbr = border_br, col = COL_BARRIER; + if (pass == 0) { + btl += barrier_outline_thick; + bbr += barrier_outline_thick; + col = COL_WIRE; } - for (dir = 1; dir < 0x10; dir <<= 1) - if (barrier(state, GX(x), GY(y)) & dir) - draw_barrier(dr, ds, x, y, dir, phase); + if (tile & (L << TILE_BARRIER_SHIFT)) + draw_rect(dr, tx, ty, btl, TILE_SIZE, col); + if (tile & (R << TILE_BARRIER_SHIFT)) + draw_rect(dr, tx+TILE_SIZE-bbr, ty, bbr, TILE_SIZE, col); + if (tile & (U << TILE_BARRIER_SHIFT)) + draw_rect(dr, tx, ty, TILE_SIZE, btl, col); + if (tile & (D << TILE_BARRIER_SHIFT)) + draw_rect(dr, tx, ty+TILE_SIZE-bbr, TILE_SIZE, bbr, col); + + if (tile & (R << TILE_BARRIER_CORNER_SHIFT)) + draw_rect(dr, tx+TILE_SIZE-bbr, ty, bbr, btl, col); + if (tile & (U << TILE_BARRIER_CORNER_SHIFT)) + draw_rect(dr, tx, ty, btl, btl, col); + if (tile & (L << TILE_BARRIER_CORNER_SHIFT)) + draw_rect(dr, tx, ty+TILE_SIZE-bbr, btl, bbr, col); + if (tile & (D << TILE_BARRIER_CORNER_SHIFT)) + draw_rect(dr, tx+TILE_SIZE-bbr, ty+TILE_SIZE-bbr, bbr, bbr, col); } + /* + * Unclip and draw update, to finish. + */ unclip(dr); - - draw_update(dr, bx, by, TILE_SIZE+TILE_BORDER, TILE_SIZE+TILE_BORDER); + draw_update(dr, clipx, clipy, clipw, cliph); } -static void game_redraw(drawing *dr, 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; + int tx, ty, dx, dy, d, dsh, last_rotate_dir, frame; unsigned char *active; + int *loops; float angle = 0.0; /* - * Clear the screen, and draw the exterior barrier lines, if - * this is our first call or if the origin has changed. + * Clear the screen on our first call. */ - if (!ds->started || ui->org_x != ds->org_x || ui->org_y != ds->org_y) { - int phase; + if (!ds->started) { + int w, h; + game_params params; ds->started = TRUE; - 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); - - ds->org_x = ui->org_x; - ds->org_y = ui->org_y; - moved_origin = TRUE; - - draw_update(dr, 0, 0, - WINDOW_OFFSET*2 + TILE_SIZE*state->width + TILE_BORDER, - WINDOW_OFFSET*2 + TILE_SIZE*state->height + TILE_BORDER); - - for (phase = 0; phase < 2; phase++) { - - for (x = 0; x < ds->width; x++) { - if (x+1 < ds->width) { - if (barrier(state, GX(x), GY(0)) & R) - draw_barrier_corner(dr, ds, x, -1, +1, +1, phase); - if (barrier(state, GX(x), GY(ds->height-1)) & R) - draw_barrier_corner(dr, ds, x, ds->height, +1, -1, phase); - } - if (barrier(state, GX(x), GY(0)) & U) { - 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(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); - } - } + params.width = ds->width; + params.height = ds->height; + game_compute_size(¶ms, TILE_SIZE, &w, &h); - for (y = 0; y < ds->height; y++) { - if (y+1 < ds->height) { - if (barrier(state, GX(0), GY(y)) & D) - draw_barrier_corner(dr, ds, -1, y, +1, +1, phase); - if (barrier(state, GX(ds->width-1), GY(y)) & D) - draw_barrier_corner(dr, ds, ds->width, y, -1, +1, phase); - } - if (barrier(state, GX(0), GY(y)) & L) { - 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(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); - } - } - } + draw_rect(dr, 0, 0, w, h, COL_BACKGROUND); + draw_update(dr, 0, 0, w, h); } tx = ty = -1; @@ -2567,28 +2873,83 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate, state = oldstate; } - frame = -1; if (ft > 0) { /* * We're animating a completion flash. Find which frame * we're at. */ frame = (int)(ft / FLASH_FRAME); + } else { + frame = 0; } /* - * Draw any tile which differs from the way it was last drawn. + * Build up a map of what we want every tile to look like. We + * include tiles one square outside the grid, for the outer edges + * of barriers. */ 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 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 && - GX(x) == ui->cur_x && GY(y) == ui->cur_y; + for (dy = -1; dy < ds->height+1; dy++) { + for (dx = -1; dx < ds->width+1; dx++) { + todraw(ds, dx, dy) = 0; + } + } + + for (dy = 0; dy < ds->height; dy++) { + int gy = (dy + ui->org_y) % ds->height; + for (dx = 0; dx < ds->width; dx++) { + int gx = (dx + ui->org_x) % ds->width; + int t = (tile(state, gx, gy) | + index(state, loops, gx, gy) | + index(state, active, gx, gy)); + + for (d = 1, dsh = 0; d < 16; d *= 2, dsh++) { + if (barrier(state, gx, gy) & d) { + todraw(ds, dx, dy) |= + d << TILE_BARRIER_SHIFT; + todraw(ds, dx + X(d), dy + Y(d)) |= + F(d) << TILE_BARRIER_SHIFT; + todraw(ds, dx + X(A(d)), dy + Y(A(d))) |= + C(d) << TILE_BARRIER_CORNER_SHIFT; + todraw(ds, dx + X(A(d)) + X(d), dy + Y(A(d)) + Y(d)) |= + F(d) << TILE_BARRIER_CORNER_SHIFT; + todraw(ds, dx + X(C(d)), dy + Y(C(d))) |= + d << TILE_BARRIER_CORNER_SHIFT; + todraw(ds, dx + X(C(d)) + X(d), dy + Y(C(d)) + Y(d)) |= + A(d) << TILE_BARRIER_CORNER_SHIFT; + } + + if (t & d) { + int edgeval = (t & LOOP(d) ? 3 : t & ACTIVE ? 2 : 1); + todraw(ds, dx, dy) |= edgeval << (TILE_WIRE_SHIFT + dsh*2); + if (!(gx == tx && gy == ty)) { + todraw(ds, dx + X(d), dy + Y(d)) |= + edgeval << (TILE_WIRE_ON_EDGE_SHIFT + (dsh ^ 2)*2); + } + } + } + + if (ui->cur_visible && gx == ui->cur_x && gy == ui->cur_y) + todraw(ds, dx, dy) |= TILE_KEYBOARD_CURSOR; + + if (gx == tx && gy == ty) + todraw(ds, dx, dy) |= TILE_ROTATING; + + if (gx == ui->cx && gy == ui->cy) { + todraw(ds, dx, dy) |= 3 << TILE_ENDPOINT_SHIFT; + } else if ((t & 0xF) != R && (t & 0xF) != U && + (t & 0xF) != L && (t & 0xF) != D) { + /* this is not an endpoint tile */ + } else if (t & ACTIVE) { + todraw(ds, dx, dy) |= 2 << TILE_ENDPOINT_SHIFT; + } else { + todraw(ds, dx, dy) |= 1 << TILE_ENDPOINT_SHIFT; + } + + if (t & LOCKED) + todraw(ds, dx, dy) |= TILE_LOCKED; /* * In a completion flash, we adjust the LOCKED bit @@ -2596,69 +2957,91 @@ static void game_redraw(drawing *dr, game_drawstate *ds, game_state *oldstate, * the frame number. */ if (frame >= 0) { - int rcx = RX(ui->cx), rcy = RY(ui->cy); + int rcx = (ui->cx + ds->width - ui->org_x) % ds->width; + int rcy = (ui->cy + ds->height - ui->org_y) % ds->height; int xdist, ydist, dist; - xdist = (x < rcx ? rcx - x : x - rcx); - ydist = (y < rcy ? rcy - y : y - rcy); + xdist = (dx < rcx ? rcx - dx : dx - rcx); + ydist = (dy < rcy ? rcy - dy : dy - rcy); dist = (xdist > ydist ? xdist : ydist); - if (frame >= dist && frame < dist+4) { - int lock = (frame - dist) & 1; - lock = lock ? LOCKED : 0; - c = (c &~ LOCKED) | lock; - } + if (frame >= dist && frame < dist+4 && + ((frame - dist) & 1)) + todraw(ds, dx, dy) ^= TILE_LOCKED; } + } + } - if (moved_origin || - index(state, ds->visible, x, y) != c || - index(state, ds->visible, x, y) == 0xFF || - is_src || is_anim || is_cursor) { - 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; - else - index(state, ds->visible, x, y) = c; + /* + * Now draw any tile that differs from the way it was last drawn. + * An exception is that if either the previous _or_ current state + * has the TILE_ROTATING bit set, we must draw it regardless, + * because it will have rotated to a different angle.q + */ + for (dy = -1; dy < ds->height+1; dy++) { + for (dx = -1; dx < ds->width+1; dx++) { + int prev = visible(ds, dx, dy); + int curr = todraw(ds, dx, dy); + if (prev != curr || ((prev | curr) & TILE_ROTATING) != 0) { + draw_tile(dr, ds, dx, dy, curr, angle); + visible(ds, dx, dy) = curr; } } + } /* * 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++; + } + + /* + * 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. */ @@ -2670,8 +3053,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 @@ -2690,17 +3073,17 @@ 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, game_ui *ui) +static int game_timing_state(const game_state *state, game_ui *ui) { return TRUE; } -static void game_print_size(game_params *params, float *x, float *y) +static void game_print_size(const game_params *params, float *x, float *y) { int pw, ph; @@ -2708,8 +3091,8 @@ static void game_print_size(game_params *params, float *x, float *y) * I'll use 8mm squares by default. */ game_compute_size(params, 800, &pw, &ph); - *x = pw / 100.0; - *y = ph / 100.0; + *x = pw / 100.0F; + *y = ph / 100.0F; } static void draw_diagram(drawing *dr, game_drawstate *ds, int x, int y, @@ -2758,7 +3141,7 @@ static void draw_diagram(drawing *dr, game_drawstate *ds, int x, int y, } } -static void game_print(drawing *dr, game_state *state, int tilesize) +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); @@ -2840,9 +3223,9 @@ static void game_print(drawing *dr, game_state *state, int tilesize) #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, @@ -2855,7 +3238,7 @@ const struct game thegame = { 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, encode_ui, @@ -2870,8 +3253,9 @@ const struct game thegame = { game_redraw, game_anim_length, game_flash_length, + game_status, TRUE, FALSE, game_print_size, game_print, - game_wants_statusbar, + TRUE, /* wants_statusbar */ FALSE, game_timing_state, - 0, /* mouse_priorities */ + 0, /* flags */ };