mirror of
git://git.tartarus.org/simon/puzzles.git
synced 2025-04-21 16:05:44 -07:00
Files

rather than literal grid descriptions, which has always faintly annoyed me because it makes it impossible to type in a grid from another source. However, Gareth pointed out that short random-seed game descriptions are useful, because you can read one out to someone else without having to master the technology of cross- machine cut and paste, or you can have two people enter the same random seed simultaneously in order to race against each other to complete the same puzzle. So both types of game ID seem to have their uses. Therefore, here's a reorganisation of the whole game ID concept. There are now two types of game ID: one has a parameter string then a hash then a piece of arbitrary random seed text, and the other has a parameter string then a colon then a literal game description. For most games, the latter is identical to the game IDs that were previously valid; for Net and Netslide, old game IDs must be translated into new ones by turning the colon into a hash, and there's a new descriptive game ID format. Random seed IDs are not guaranteed to be portable between software versions (this is a major reason why I added version reporting yesterday). Descriptive game IDs have a longer lifespan. As an added bonus, I've removed the sections of documentation dealing with game parameter encodings not shown in the game ID (Rectangles expansion factor, Solo symmetry and difficulty settings etc), because _all_ parameters must be specified in a random seed ID and therefore users can easily find out the appropriate parameter string for any settings they have configured. [originally from svn r5788]
1651 lines
47 KiB
C
1651 lines
47 KiB
C
/*
|
|
* net.c: Net game.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <math.h>
|
|
|
|
#include "puzzles.h"
|
|
#include "tree234.h"
|
|
|
|
#define PI 3.141592653589793238462643383279502884197169399
|
|
|
|
#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)
|
|
|
|
/* Direction and other bitfields */
|
|
#define R 0x01
|
|
#define U 0x02
|
|
#define L 0x04
|
|
#define D 0x08
|
|
#define LOCKED 0x10
|
|
#define ACTIVE 0x20
|
|
/* Corner flags go in the barriers array */
|
|
#define RU 0x10
|
|
#define UL 0x20
|
|
#define LD 0x40
|
|
#define DR 0x80
|
|
|
|
/* Rotations: Anticlockwise, Clockwise, Flip, general rotate */
|
|
#define A(x) ( (((x) & 0x07) << 1) | (((x) & 0x08) >> 3) )
|
|
#define C(x) ( (((x) & 0x0E) >> 1) | (((x) & 0x01) << 3) )
|
|
#define F(x) ( (((x) & 0x0C) >> 2) | (((x) & 0x03) << 2) )
|
|
#define ROT(x, n) ( ((n)&3) == 0 ? (x) : \
|
|
((n)&3) == 1 ? A(x) : \
|
|
((n)&3) == 2 ? F(x) : C(x) )
|
|
|
|
/* X and Y displacements */
|
|
#define X(x) ( (x) == R ? +1 : (x) == L ? -1 : 0 )
|
|
#define Y(x) ( (x) == D ? +1 : (x) == U ? -1 : 0 )
|
|
|
|
/* Bit count */
|
|
#define COUNT(x) ( (((x) & 0x08) >> 3) + (((x) & 0x04) >> 2) + \
|
|
(((x) & 0x02) >> 1) + ((x) & 0x01) )
|
|
|
|
#define TILE_SIZE 32
|
|
#define TILE_BORDER 1
|
|
#define WINDOW_OFFSET 16
|
|
|
|
#define ROTATE_TIME 0.13F
|
|
#define FLASH_FRAME 0.07F
|
|
|
|
enum {
|
|
COL_BACKGROUND,
|
|
COL_LOCKED,
|
|
COL_BORDER,
|
|
COL_WIRE,
|
|
COL_ENDPOINT,
|
|
COL_POWERED,
|
|
COL_BARRIER,
|
|
NCOLOURS
|
|
};
|
|
|
|
struct game_params {
|
|
int width;
|
|
int height;
|
|
int wrapping;
|
|
float barrier_probability;
|
|
};
|
|
|
|
struct game_aux_info {
|
|
int width, height;
|
|
unsigned char *tiles;
|
|
};
|
|
|
|
struct game_state {
|
|
int width, height, cx, cy, wrapping, completed;
|
|
int last_rotate_x, last_rotate_y, last_rotate_dir;
|
|
int used_solve, just_used_solve;
|
|
unsigned char *tiles;
|
|
unsigned char *barriers;
|
|
};
|
|
|
|
#define OFFSET(x2,y2,x1,y1,dir,state) \
|
|
( (x2) = ((x1) + (state)->width + X((dir))) % (state)->width, \
|
|
(y2) = ((y1) + (state)->height + Y((dir))) % (state)->height)
|
|
|
|
#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)
|
|
|
|
struct xyd {
|
|
int x, y, direction;
|
|
};
|
|
|
|
static int xyd_cmp(void *av, void *bv) {
|
|
struct xyd *a = (struct xyd *)av;
|
|
struct xyd *b = (struct xyd *)bv;
|
|
if (a->x < b->x)
|
|
return -1;
|
|
if (a->x > b->x)
|
|
return +1;
|
|
if (a->y < b->y)
|
|
return -1;
|
|
if (a->y > b->y)
|
|
return +1;
|
|
if (a->direction < b->direction)
|
|
return -1;
|
|
if (a->direction > b->direction)
|
|
return +1;
|
|
return 0;
|
|
};
|
|
|
|
static struct xyd *new_xyd(int x, int y, int direction)
|
|
{
|
|
struct xyd *xyd = snew(struct xyd);
|
|
xyd->x = x;
|
|
xyd->y = y;
|
|
xyd->direction = direction;
|
|
return xyd;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
* Manage game parameters.
|
|
*/
|
|
static game_params *default_params(void)
|
|
{
|
|
game_params *ret = snew(game_params);
|
|
|
|
ret->width = 5;
|
|
ret->height = 5;
|
|
ret->wrapping = FALSE;
|
|
ret->barrier_probability = 0.0;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int game_fetch_preset(int i, char **name, game_params **params)
|
|
{
|
|
game_params *ret;
|
|
char str[80];
|
|
static const struct { int x, y, wrap; } values[] = {
|
|
{5, 5, FALSE},
|
|
{7, 7, FALSE},
|
|
{9, 9, FALSE},
|
|
{11, 11, FALSE},
|
|
{13, 11, FALSE},
|
|
{5, 5, TRUE},
|
|
{7, 7, TRUE},
|
|
{9, 9, TRUE},
|
|
{11, 11, TRUE},
|
|
{13, 11, TRUE},
|
|
};
|
|
|
|
if (i < 0 || i >= lenof(values))
|
|
return FALSE;
|
|
|
|
ret = snew(game_params);
|
|
ret->width = values[i].x;
|
|
ret->height = values[i].y;
|
|
ret->wrapping = values[i].wrap;
|
|
ret->barrier_probability = 0.0;
|
|
|
|
sprintf(str, "%dx%d%s", ret->width, ret->height,
|
|
ret->wrapping ? " wrapping" : "");
|
|
|
|
*name = dupstr(str);
|
|
*params = ret;
|
|
return TRUE;
|
|
}
|
|
|
|
static void free_params(game_params *params)
|
|
{
|
|
sfree(params);
|
|
}
|
|
|
|
static game_params *dup_params(game_params *params)
|
|
{
|
|
game_params *ret = snew(game_params);
|
|
*ret = *params; /* structure copy */
|
|
return ret;
|
|
}
|
|
|
|
static void decode_params(game_params *ret, char const *string)
|
|
{
|
|
char const *p = string;
|
|
|
|
ret->width = atoi(p);
|
|
while (*p && isdigit(*p)) p++;
|
|
if (*p == 'x') {
|
|
p++;
|
|
ret->height = atoi(p);
|
|
while (*p && isdigit(*p)) p++;
|
|
if ( (ret->wrapping = (*p == 'w')) != 0 )
|
|
p++;
|
|
if (*p == 'b')
|
|
ret->barrier_probability = atof(p+1);
|
|
} else {
|
|
ret->height = ret->width;
|
|
}
|
|
}
|
|
|
|
static char *encode_params(game_params *params, int full)
|
|
{
|
|
char ret[400];
|
|
int len;
|
|
|
|
len = sprintf(ret, "%dx%d", params->width, params->height);
|
|
if (params->wrapping)
|
|
ret[len++] = 'w';
|
|
if (full && params->barrier_probability)
|
|
len += sprintf(ret+len, "b%g", params->barrier_probability);
|
|
assert(len < lenof(ret));
|
|
ret[len] = '\0';
|
|
|
|
return dupstr(ret);
|
|
}
|
|
|
|
static config_item *game_configure(game_params *params)
|
|
{
|
|
config_item *ret;
|
|
char buf[80];
|
|
|
|
ret = snewn(5, config_item);
|
|
|
|
ret[0].name = "Width";
|
|
ret[0].type = C_STRING;
|
|
sprintf(buf, "%d", params->width);
|
|
ret[0].sval = dupstr(buf);
|
|
ret[0].ival = 0;
|
|
|
|
ret[1].name = "Height";
|
|
ret[1].type = C_STRING;
|
|
sprintf(buf, "%d", params->height);
|
|
ret[1].sval = dupstr(buf);
|
|
ret[1].ival = 0;
|
|
|
|
ret[2].name = "Walls wrap around";
|
|
ret[2].type = C_BOOLEAN;
|
|
ret[2].sval = NULL;
|
|
ret[2].ival = params->wrapping;
|
|
|
|
ret[3].name = "Barrier probability";
|
|
ret[3].type = C_STRING;
|
|
sprintf(buf, "%g", params->barrier_probability);
|
|
ret[3].sval = dupstr(buf);
|
|
ret[3].ival = 0;
|
|
|
|
ret[4].name = NULL;
|
|
ret[4].type = C_END;
|
|
ret[4].sval = NULL;
|
|
ret[4].ival = 0;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static game_params *custom_params(config_item *cfg)
|
|
{
|
|
game_params *ret = snew(game_params);
|
|
|
|
ret->width = atoi(cfg[0].sval);
|
|
ret->height = atoi(cfg[1].sval);
|
|
ret->wrapping = cfg[2].ival;
|
|
ret->barrier_probability = (float)atof(cfg[3].sval);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static char *validate_params(game_params *params)
|
|
{
|
|
if (params->width <= 0 && params->height <= 0)
|
|
return "Width and height must both be greater than zero";
|
|
if (params->width <= 0)
|
|
return "Width must be greater than zero";
|
|
if (params->height <= 0)
|
|
return "Height must be greater than zero";
|
|
if (params->width <= 1 && params->height <= 1)
|
|
return "At least one of width and height must be greater than one";
|
|
if (params->barrier_probability < 0)
|
|
return "Barrier probability may not be negative";
|
|
if (params->barrier_probability > 1)
|
|
return "Barrier probability may not be greater than 1";
|
|
return NULL;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
* Randomly select a new game description.
|
|
*/
|
|
|
|
static char *new_game_desc(game_params *params, random_state *rs,
|
|
game_aux_info **aux)
|
|
{
|
|
tree234 *possibilities, *barriertree;
|
|
int w, h, x, y, cx, cy, nbarriers;
|
|
unsigned char *tiles, *barriers;
|
|
char *desc, *p;
|
|
|
|
w = params->width;
|
|
h = params->height;
|
|
|
|
tiles = snewn(w * h, unsigned char);
|
|
memset(tiles, 0, w * h);
|
|
barriers = snewn(w * h, unsigned char);
|
|
memset(barriers, 0, w * h);
|
|
|
|
cx = w / 2;
|
|
cy = h / 2;
|
|
|
|
/*
|
|
* Construct the unshuffled grid.
|
|
*
|
|
* To do this, we simply start at the centre point, repeatedly
|
|
* choose a random possibility out of the available ways to
|
|
* extend a used square into an unused one, and do it. After
|
|
* extending the third line out of a square, we remove the
|
|
* fourth from the possibilities list to avoid any full-cross
|
|
* squares (which would make the game too easy because they
|
|
* only have one orientation).
|
|
*
|
|
* The slightly worrying thing is the avoidance of full-cross
|
|
* squares. Can this cause our unsophisticated construction
|
|
* algorithm to paint itself into a corner, by getting into a
|
|
* situation where there are some unreached squares and the
|
|
* only way to reach any of them is to extend a T-piece into a
|
|
* full cross?
|
|
*
|
|
* Answer: no it can't, and here's a proof.
|
|
*
|
|
* Any contiguous group of such unreachable squares must be
|
|
* surrounded on _all_ sides by T-pieces pointing away from the
|
|
* group. (If not, then there is a square which can be extended
|
|
* into one of the `unreachable' ones, and so it wasn't
|
|
* unreachable after all.) In particular, this implies that
|
|
* each contiguous group of unreachable squares must be
|
|
* rectangular in shape (any deviation from that yields a
|
|
* non-T-piece next to an `unreachable' square).
|
|
*
|
|
* So we have a rectangle of unreachable squares, with T-pieces
|
|
* forming a solid border around the rectangle. The corners of
|
|
* that border must be connected (since every tile connects all
|
|
* the lines arriving in it), and therefore the border must
|
|
* form a closed loop around the rectangle.
|
|
*
|
|
* But this can't have happened in the first place, since we
|
|
* _know_ we've avoided creating closed loops! Hence, no such
|
|
* situation can ever arise, and the naive grid construction
|
|
* algorithm will guaranteeably result in a complete grid
|
|
* containing no unreached squares, no full crosses _and_ no
|
|
* closed loops. []
|
|
*/
|
|
possibilities = newtree234(xyd_cmp);
|
|
|
|
if (cx+1 < w)
|
|
add234(possibilities, new_xyd(cx, cy, R));
|
|
if (cy-1 >= 0)
|
|
add234(possibilities, new_xyd(cx, cy, U));
|
|
if (cx-1 >= 0)
|
|
add234(possibilities, new_xyd(cx, cy, L));
|
|
if (cy+1 < h)
|
|
add234(possibilities, new_xyd(cx, cy, D));
|
|
|
|
while (count234(possibilities) > 0) {
|
|
int i;
|
|
struct xyd *xyd;
|
|
int x1, y1, d1, x2, y2, d2, d;
|
|
|
|
/*
|
|
* Extract a randomly chosen possibility from the list.
|
|
*/
|
|
i = random_upto(rs, count234(possibilities));
|
|
xyd = delpos234(possibilities, i);
|
|
x1 = xyd->x;
|
|
y1 = xyd->y;
|
|
d1 = xyd->direction;
|
|
sfree(xyd);
|
|
|
|
OFFSET(x2, y2, x1, y1, d1, params);
|
|
d2 = F(d1);
|
|
#ifdef DEBUG
|
|
printf("picked (%d,%d,%c) <-> (%d,%d,%c)\n",
|
|
x1, y1, "0RU3L567D9abcdef"[d1], x2, y2, "0RU3L567D9abcdef"[d2]);
|
|
#endif
|
|
|
|
/*
|
|
* Make the connection. (We should be moving to an as yet
|
|
* unused tile.)
|
|
*/
|
|
index(params, tiles, x1, y1) |= d1;
|
|
assert(index(params, tiles, x2, y2) == 0);
|
|
index(params, tiles, x2, y2) |= d2;
|
|
|
|
/*
|
|
* If we have created a T-piece, remove its last
|
|
* possibility.
|
|
*/
|
|
if (COUNT(index(params, tiles, x1, y1)) == 3) {
|
|
struct xyd xyd1, *xydp;
|
|
|
|
xyd1.x = x1;
|
|
xyd1.y = y1;
|
|
xyd1.direction = 0x0F ^ index(params, tiles, x1, y1);
|
|
|
|
xydp = find234(possibilities, &xyd1, NULL);
|
|
|
|
if (xydp) {
|
|
#ifdef DEBUG
|
|
printf("T-piece; removing (%d,%d,%c)\n",
|
|
xydp->x, xydp->y, "0RU3L567D9abcdef"[xydp->direction]);
|
|
#endif
|
|
del234(possibilities, xydp);
|
|
sfree(xydp);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Remove all other possibilities that were pointing at the
|
|
* tile we've just moved into.
|
|
*/
|
|
for (d = 1; d < 0x10; d <<= 1) {
|
|
int x3, y3, d3;
|
|
struct xyd xyd1, *xydp;
|
|
|
|
OFFSET(x3, y3, x2, y2, d, params);
|
|
d3 = F(d);
|
|
|
|
xyd1.x = x3;
|
|
xyd1.y = y3;
|
|
xyd1.direction = d3;
|
|
|
|
xydp = find234(possibilities, &xyd1, NULL);
|
|
|
|
if (xydp) {
|
|
#ifdef DEBUG
|
|
printf("Loop avoidance; removing (%d,%d,%c)\n",
|
|
xydp->x, xydp->y, "0RU3L567D9abcdef"[xydp->direction]);
|
|
#endif
|
|
del234(possibilities, xydp);
|
|
sfree(xydp);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Add new possibilities to the list for moving _out_ of
|
|
* the tile we have just moved into.
|
|
*/
|
|
for (d = 1; d < 0x10; d <<= 1) {
|
|
int x3, y3;
|
|
|
|
if (d == d2)
|
|
continue; /* we've got this one already */
|
|
|
|
if (!params->wrapping) {
|
|
if (d == U && y2 == 0)
|
|
continue;
|
|
if (d == D && y2 == h-1)
|
|
continue;
|
|
if (d == L && x2 == 0)
|
|
continue;
|
|
if (d == R && x2 == w-1)
|
|
continue;
|
|
}
|
|
|
|
OFFSET(x3, y3, x2, y2, d, params);
|
|
|
|
if (index(params, tiles, x3, y3))
|
|
continue; /* this would create a loop */
|
|
|
|
#ifdef DEBUG
|
|
printf("New frontier; adding (%d,%d,%c)\n",
|
|
x2, y2, "0RU3L567D9abcdef"[d]);
|
|
#endif
|
|
add234(possibilities, new_xyd(x2, y2, d));
|
|
}
|
|
}
|
|
/* Having done that, we should have no possibilities remaining. */
|
|
assert(count234(possibilities) == 0);
|
|
freetree234(possibilities);
|
|
|
|
/*
|
|
* Now compute a list of the possible barrier locations.
|
|
*/
|
|
barriertree = newtree234(xyd_cmp);
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
|
|
if (!(index(params, tiles, x, y) & R) &&
|
|
(params->wrapping || x < w-1))
|
|
add234(barriertree, new_xyd(x, y, R));
|
|
if (!(index(params, tiles, x, y) & D) &&
|
|
(params->wrapping || y < h-1))
|
|
add234(barriertree, new_xyd(x, y, D));
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Save the unshuffled grid in an aux_info.
|
|
*/
|
|
{
|
|
game_aux_info *solution;
|
|
|
|
solution = snew(game_aux_info);
|
|
solution->width = w;
|
|
solution->height = h;
|
|
solution->tiles = snewn(w * h, unsigned char);
|
|
memcpy(solution->tiles, tiles, w * h);
|
|
|
|
*aux = solution;
|
|
}
|
|
|
|
/*
|
|
* Now shuffle the grid.
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* And now choose barrier locations. (We carefully do this
|
|
* _after_ shuffling, so that changing the barrier rate in the
|
|
* params while keeping the random seed the same will give the
|
|
* same shuffled grid and _only_ change the barrier locations.
|
|
* Also the way we choose barrier locations, by repeatedly
|
|
* choosing one possibility from the list until we have enough,
|
|
* is designed to ensure that raising the barrier rate while
|
|
* keeping the seed the same will provide a superset of the
|
|
* previous barrier set - i.e. if you ask for 10 barriers, and
|
|
* then decide that's still too hard and ask for 20, you'll get
|
|
* the original 10 plus 10 more, rather than getting 20 new
|
|
* ones and the chance of remembering your first 10.)
|
|
*/
|
|
nbarriers = (int)(params->barrier_probability * count234(barriertree));
|
|
assert(nbarriers >= 0 && nbarriers <= count234(barriertree));
|
|
|
|
while (nbarriers > 0) {
|
|
int i;
|
|
struct xyd *xyd;
|
|
int x1, y1, d1, x2, y2, d2;
|
|
|
|
/*
|
|
* Extract a randomly chosen barrier from the list.
|
|
*/
|
|
i = random_upto(rs, count234(barriertree));
|
|
xyd = delpos234(barriertree, i);
|
|
|
|
assert(xyd != NULL);
|
|
|
|
x1 = xyd->x;
|
|
y1 = xyd->y;
|
|
d1 = xyd->direction;
|
|
sfree(xyd);
|
|
|
|
OFFSET(x2, y2, x1, y1, d1, params);
|
|
d2 = F(d1);
|
|
|
|
index(params, barriers, x1, y1) |= d1;
|
|
index(params, barriers, x2, y2) |= d2;
|
|
|
|
nbarriers--;
|
|
}
|
|
|
|
/*
|
|
* Clean up the rest of the barrier list.
|
|
*/
|
|
{
|
|
struct xyd *xyd;
|
|
|
|
while ( (xyd = delpos234(barriertree, 0)) != NULL)
|
|
sfree(xyd);
|
|
|
|
freetree234(barriertree);
|
|
}
|
|
|
|
/*
|
|
* Finally, encode the grid into a string game description.
|
|
*
|
|
* My syntax is extremely simple: each square is encoded as a
|
|
* hex digit in which bit 0 means a connection on the right,
|
|
* bit 1 means up, bit 2 left and bit 3 down. (i.e. the same
|
|
* encoding as used internally). Each digit is followed by
|
|
* optional barrier indicators: `v' means a vertical barrier to
|
|
* the right of it, and `h' means a horizontal barrier below
|
|
* it.
|
|
*/
|
|
desc = snewn(w * h * 3 + 1, char);
|
|
p = desc;
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
*p++ = "0123456789abcdef"[index(params, tiles, x, y)];
|
|
if ((params->wrapping || x < w-1) &&
|
|
(index(params, barriers, x, y) & R))
|
|
*p++ = 'v';
|
|
if ((params->wrapping || y < h-1) &&
|
|
(index(params, barriers, x, y) & D))
|
|
*p++ = 'h';
|
|
}
|
|
}
|
|
assert(p - desc <= w*h*3);
|
|
|
|
sfree(tiles);
|
|
sfree(barriers);
|
|
|
|
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)
|
|
{
|
|
int w = params->width, h = params->height;
|
|
int i;
|
|
|
|
for (i = 0; i < w*h; i++) {
|
|
if (*desc >= '0' && *desc <= '9')
|
|
/* OK */;
|
|
else if (*desc >= 'a' && *desc <= 'f')
|
|
/* OK */;
|
|
else if (*desc >= 'A' && *desc <= 'F')
|
|
/* OK */;
|
|
else if (!*desc)
|
|
return "Game description shorter than expected";
|
|
else
|
|
return "Game description contained unexpected character";
|
|
desc++;
|
|
while (*desc == 'h' || *desc == 'v')
|
|
desc++;
|
|
}
|
|
if (*desc)
|
|
return "Game description longer than expected";
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
* Construct an initial game state, given a description and parameters.
|
|
*/
|
|
|
|
static game_state *new_game(game_params *params, char *desc)
|
|
{
|
|
game_state *state;
|
|
int w, h, x, y;
|
|
|
|
assert(params->width > 0 && params->height > 0);
|
|
assert(params->width > 1 || params->height > 1);
|
|
|
|
/*
|
|
* Create a blank game state.
|
|
*/
|
|
state = snew(game_state);
|
|
w = state->width = params->width;
|
|
h = state->height = params->height;
|
|
state->cx = state->width / 2;
|
|
state->cy = state->height / 2;
|
|
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->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);
|
|
|
|
/*
|
|
* Parse the game description into the grid.
|
|
*/
|
|
for (y = 0; y < h; y++) {
|
|
for (x = 0; x < w; x++) {
|
|
if (*desc >= '0' && *desc <= '9')
|
|
tile(state, x, y) = *desc - '0';
|
|
else if (*desc >= 'a' && *desc <= 'f')
|
|
tile(state, x, y) = *desc - 'a' + 10;
|
|
else if (*desc >= 'A' && *desc <= 'F')
|
|
tile(state, x, y) = *desc - 'A' + 10;
|
|
if (*desc)
|
|
desc++;
|
|
while (*desc == 'h' || *desc == 'v') {
|
|
int x2, y2, d1, d2;
|
|
if (*desc == 'v')
|
|
d1 = R;
|
|
else
|
|
d1 = D;
|
|
|
|
OFFSET(x2, y2, x, y, d1, state);
|
|
d2 = F(d1);
|
|
|
|
barrier(state, x, y) |= d1;
|
|
barrier(state, x2, y2) |= d2;
|
|
|
|
desc++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Set up border barriers if this is a non-wrapping game.
|
|
*/
|
|
if (!state->wrapping) {
|
|
for (x = 0; x < state->width; x++) {
|
|
barrier(state, x, 0) |= U;
|
|
barrier(state, x, state->height-1) |= D;
|
|
}
|
|
for (y = 0; y < state->height; y++) {
|
|
barrier(state, 0, y) |= L;
|
|
barrier(state, state->width-1, y) |= R;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Set up the barrier corner flags, for drawing barriers
|
|
* prettily when they meet.
|
|
*/
|
|
for (y = 0; y < state->height; y++) {
|
|
for (x = 0; x < state->width; x++) {
|
|
int dir;
|
|
|
|
for (dir = 1; dir < 0x10; dir <<= 1) {
|
|
int dir2 = A(dir);
|
|
int x1, y1, x2, y2, x3, y3;
|
|
int corner = FALSE;
|
|
|
|
if (!(barrier(state, x, y) & dir))
|
|
continue;
|
|
|
|
if (barrier(state, x, y) & dir2)
|
|
corner = TRUE;
|
|
|
|
x1 = x + X(dir), y1 = y + Y(dir);
|
|
if (x1 >= 0 && x1 < state->width &&
|
|
y1 >= 0 && y1 < state->height &&
|
|
(barrier(state, x1, y1) & dir2))
|
|
corner = TRUE;
|
|
|
|
x2 = x + X(dir2), y2 = y + Y(dir2);
|
|
if (x2 >= 0 && x2 < state->width &&
|
|
y2 >= 0 && y2 < state->height &&
|
|
(barrier(state, x2, y2) & dir))
|
|
corner = TRUE;
|
|
|
|
if (corner) {
|
|
barrier(state, x, y) |= (dir << 4);
|
|
if (x1 >= 0 && x1 < state->width &&
|
|
y1 >= 0 && y1 < state->height)
|
|
barrier(state, x1, y1) |= (A(dir) << 4);
|
|
if (x2 >= 0 && x2 < state->width &&
|
|
y2 >= 0 && y2 < state->height)
|
|
barrier(state, x2, y2) |= (C(dir) << 4);
|
|
x3 = x + X(dir) + X(dir2), y3 = y + Y(dir) + Y(dir2);
|
|
if (x3 >= 0 && x3 < state->width &&
|
|
y3 >= 0 && y3 < state->height)
|
|
barrier(state, x3, y3) |= (F(dir) << 4);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
static game_state *dup_game(game_state *state)
|
|
{
|
|
game_state *ret;
|
|
|
|
ret = snew(game_state);
|
|
ret->width = state->width;
|
|
ret->height = state->height;
|
|
ret->cx = state->cx;
|
|
ret->cy = state->cy;
|
|
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)
|
|
{
|
|
sfree(state->tiles);
|
|
sfree(state->barriers);
|
|
sfree(state);
|
|
}
|
|
|
|
static game_state *solve_game(game_state *state, game_aux_info *aux,
|
|
char **error)
|
|
{
|
|
game_state *ret;
|
|
|
|
if (!aux) {
|
|
*error = "Solution not known for this puzzle";
|
|
return NULL;
|
|
}
|
|
|
|
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;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static char *game_text_format(game_state *state)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
* Utility routine.
|
|
*/
|
|
|
|
/*
|
|
* Compute which squares are reachable from the centre square, as a
|
|
* quick visual aid to determining how close the game is to
|
|
* completion. This is also a simple way to tell if the game _is_
|
|
* completed - just call this function and see whether every square
|
|
* is marked active.
|
|
*/
|
|
static unsigned char *compute_active(game_state *state)
|
|
{
|
|
unsigned char *active;
|
|
tree234 *todo;
|
|
struct xyd *xyd;
|
|
|
|
active = snewn(state->width * state->height, unsigned char);
|
|
memset(active, 0, state->width * state->height);
|
|
|
|
/*
|
|
* We only store (x,y) pairs in todo, but it's easier to reuse
|
|
* xyd_cmp and just store direction 0 every time.
|
|
*/
|
|
todo = newtree234(xyd_cmp);
|
|
index(state, active, state->cx, state->cy) = ACTIVE;
|
|
add234(todo, new_xyd(state->cx, state->cy, 0));
|
|
|
|
while ( (xyd = delpos234(todo, 0)) != NULL) {
|
|
int x1, y1, d1, x2, y2, d2;
|
|
|
|
x1 = xyd->x;
|
|
y1 = xyd->y;
|
|
sfree(xyd);
|
|
|
|
for (d1 = 1; d1 < 0x10; d1 <<= 1) {
|
|
OFFSET(x2, y2, x1, y1, d1, state);
|
|
d2 = F(d1);
|
|
|
|
/*
|
|
* If the next tile in this direction is connected to
|
|
* us, and there isn't a barrier in the way, and it
|
|
* isn't already marked active, then mark it active and
|
|
* add it to the to-examine list.
|
|
*/
|
|
if ((tile(state, x1, y1) & d1) &&
|
|
(tile(state, x2, y2) & d2) &&
|
|
!(barrier(state, x1, y1) & d1) &&
|
|
!index(state, active, x2, y2)) {
|
|
index(state, active, x2, y2) = ACTIVE;
|
|
add234(todo, new_xyd(x2, y2, 0));
|
|
}
|
|
}
|
|
}
|
|
/* Now we expect the todo list to have shrunk to zero size. */
|
|
assert(count234(todo) == 0);
|
|
freetree234(todo);
|
|
|
|
return active;
|
|
}
|
|
|
|
struct game_ui {
|
|
int cur_x, cur_y;
|
|
int cur_visible;
|
|
random_state *rs; /* used for jumbling */
|
|
};
|
|
|
|
static game_ui *new_ui(game_state *state)
|
|
{
|
|
void *seed;
|
|
int seedsize;
|
|
game_ui *ui = snew(game_ui);
|
|
ui->cur_x = state->width / 2;
|
|
ui->cur_y = state->height / 2;
|
|
ui->cur_visible = FALSE;
|
|
get_random_seed(&seed, &seedsize);
|
|
ui->rs = random_init(seed, seedsize);
|
|
sfree(seed);
|
|
|
|
return ui;
|
|
}
|
|
|
|
static void free_ui(game_ui *ui)
|
|
{
|
|
random_free(ui->rs);
|
|
sfree(ui);
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
* Process a move.
|
|
*/
|
|
static game_state *make_move(game_state *state, game_ui *ui,
|
|
int x, int y, int button)
|
|
{
|
|
game_state *ret, *nullret;
|
|
int tx, ty, orig;
|
|
|
|
nullret = NULL;
|
|
|
|
if (button == LEFT_BUTTON ||
|
|
button == MIDDLE_BUTTON ||
|
|
button == RIGHT_BUTTON) {
|
|
|
|
if (ui->cur_visible) {
|
|
ui->cur_visible = FALSE;
|
|
nullret = state;
|
|
}
|
|
|
|
/*
|
|
* The button must have been clicked on a valid tile.
|
|
*/
|
|
x -= WINDOW_OFFSET + TILE_BORDER;
|
|
y -= WINDOW_OFFSET + TILE_BORDER;
|
|
if (x < 0 || y < 0)
|
|
return nullret;
|
|
tx = x / TILE_SIZE;
|
|
ty = y / TILE_SIZE;
|
|
if (tx >= state->width || ty >= state->height)
|
|
return nullret;
|
|
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) {
|
|
if (button == CURSOR_UP && ui->cur_y > 0)
|
|
ui->cur_y--;
|
|
else if (button == CURSOR_DOWN && ui->cur_y < state->height-1)
|
|
ui->cur_y++;
|
|
else if (button == CURSOR_LEFT && ui->cur_x > 0)
|
|
ui->cur_x--;
|
|
else if (button == CURSOR_RIGHT && ui->cur_x < state->width-1)
|
|
ui->cur_x++;
|
|
else
|
|
return nullret; /* no cursor movement */
|
|
ui->cur_visible = TRUE;
|
|
return state; /* UI activity has occurred */
|
|
} else if (button == 'a' || button == 's' || button == 'd' ||
|
|
button == 'A' || button == 'S' || button == 'D') {
|
|
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;
|
|
else if (button == 'd' || button == 'D')
|
|
button = RIGHT_BUTTON;
|
|
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 :( */
|
|
} else
|
|
return nullret;
|
|
|
|
/*
|
|
* The middle button locks or unlocks a tile. (A locked tile
|
|
* cannot be turned, and is visually marked as being locked.
|
|
* This is a convenience for the player, so that once they are
|
|
* sure which way round a tile goes, they can lock it and thus
|
|
* avoid forgetting later on that they'd already done that one;
|
|
* and the locking also prevents them turning the tile by
|
|
* 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) {
|
|
|
|
/*
|
|
* The left and right buttons have no effect if clicked on a
|
|
* locked tile.
|
|
*/
|
|
if (tile(state, tx, ty) & LOCKED)
|
|
return nullret;
|
|
|
|
/*
|
|
* 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') {
|
|
|
|
/*
|
|
* 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 rot = random_upto(ui->rs, 4);
|
|
orig = tile(ret, jx, jy);
|
|
tile(ret, jx, jy) = ROT(orig, rot);
|
|
}
|
|
}
|
|
}
|
|
ret->last_rotate_dir = 0; /* suppress animation */
|
|
ret->last_rotate_x = ret->last_rotate_y = 0;
|
|
|
|
} else assert(0);
|
|
|
|
/*
|
|
* Check whether the game has been completed.
|
|
*/
|
|
{
|
|
unsigned char *active = compute_active(ret);
|
|
int x1, y1;
|
|
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)) {
|
|
complete = FALSE;
|
|
goto break_label; /* break out of two loops at once */
|
|
}
|
|
break_label:
|
|
|
|
sfree(active);
|
|
|
|
if (complete)
|
|
ret->completed = TRUE;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
* Routines for drawing the game position on the screen.
|
|
*/
|
|
|
|
struct game_drawstate {
|
|
int started;
|
|
int width, height;
|
|
unsigned char *visible;
|
|
};
|
|
|
|
static game_drawstate *game_new_drawstate(game_state *state)
|
|
{
|
|
game_drawstate *ds = snew(game_drawstate);
|
|
|
|
ds->started = FALSE;
|
|
ds->width = state->width;
|
|
ds->height = state->height;
|
|
ds->visible = snewn(state->width * state->height, unsigned char);
|
|
memset(ds->visible, 0xFF, state->width * state->height);
|
|
|
|
return ds;
|
|
}
|
|
|
|
static void game_free_drawstate(game_drawstate *ds)
|
|
{
|
|
sfree(ds->visible);
|
|
sfree(ds);
|
|
}
|
|
|
|
static void game_size(game_params *params, int *x, int *y)
|
|
{
|
|
*x = WINDOW_OFFSET * 2 + TILE_SIZE * params->width + TILE_BORDER;
|
|
*y = WINDOW_OFFSET * 2 + TILE_SIZE * params->height + TILE_BORDER;
|
|
}
|
|
|
|
static float *game_colours(frontend *fe, game_state *state, int *ncolours)
|
|
{
|
|
float *ret;
|
|
|
|
ret = snewn(NCOLOURS * 3, float);
|
|
*ncolours = NCOLOURS;
|
|
|
|
/*
|
|
* Basic background colour is whatever the front end thinks is
|
|
* a sensible default.
|
|
*/
|
|
frontend_default_colour(fe, &ret[COL_BACKGROUND * 3]);
|
|
|
|
/*
|
|
* Wires are black.
|
|
*/
|
|
ret[COL_WIRE * 3 + 0] = 0.0F;
|
|
ret[COL_WIRE * 3 + 1] = 0.0F;
|
|
ret[COL_WIRE * 3 + 2] = 0.0F;
|
|
|
|
/*
|
|
* Powered wires and powered endpoints are cyan.
|
|
*/
|
|
ret[COL_POWERED * 3 + 0] = 0.0F;
|
|
ret[COL_POWERED * 3 + 1] = 1.0F;
|
|
ret[COL_POWERED * 3 + 2] = 1.0F;
|
|
|
|
/*
|
|
* Barriers are red.
|
|
*/
|
|
ret[COL_BARRIER * 3 + 0] = 1.0F;
|
|
ret[COL_BARRIER * 3 + 1] = 0.0F;
|
|
ret[COL_BARRIER * 3 + 2] = 0.0F;
|
|
|
|
/*
|
|
* Unpowered endpoints are blue.
|
|
*/
|
|
ret[COL_ENDPOINT * 3 + 0] = 0.0F;
|
|
ret[COL_ENDPOINT * 3 + 1] = 0.0F;
|
|
ret[COL_ENDPOINT * 3 + 2] = 1.0F;
|
|
|
|
/*
|
|
* Tile borders are a darker grey than the background.
|
|
*/
|
|
ret[COL_BORDER * 3 + 0] = 0.5F * ret[COL_BACKGROUND * 3 + 0];
|
|
ret[COL_BORDER * 3 + 1] = 0.5F * ret[COL_BACKGROUND * 3 + 1];
|
|
ret[COL_BORDER * 3 + 2] = 0.5F * ret[COL_BACKGROUND * 3 + 2];
|
|
|
|
/*
|
|
* Locked tiles are a grey in between those two.
|
|
*/
|
|
ret[COL_LOCKED * 3 + 0] = 0.75F * ret[COL_BACKGROUND * 3 + 0];
|
|
ret[COL_LOCKED * 3 + 1] = 0.75F * ret[COL_BACKGROUND * 3 + 1];
|
|
ret[COL_LOCKED * 3 + 2] = 0.75F * ret[COL_BACKGROUND * 3 + 2];
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void draw_thick_line(frontend *fe, 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);
|
|
}
|
|
|
|
static void draw_rect_coords(frontend *fe, int x1, int y1, int x2, int y2,
|
|
int colour)
|
|
{
|
|
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(fe, mx, my, dx, dy, colour);
|
|
}
|
|
|
|
static void draw_barrier_corner(frontend *fe, 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, dx, dy, dir2;
|
|
|
|
dir >>= 4;
|
|
|
|
dir2 = A(dir);
|
|
dx = X(dir) + X(dir2);
|
|
dy = Y(dir) + Y(dir2);
|
|
x1 = (dx > 0 ? TILE_SIZE+TILE_BORDER-1 : 0);
|
|
y1 = (dy > 0 ? TILE_SIZE+TILE_BORDER-1 : 0);
|
|
|
|
if (phase == 0) {
|
|
draw_rect_coords(fe, bx+x1, by+y1,
|
|
bx+x1-TILE_BORDER*dx, by+y1-(TILE_BORDER-1)*dy,
|
|
COL_WIRE);
|
|
draw_rect_coords(fe, bx+x1, by+y1,
|
|
bx+x1-(TILE_BORDER-1)*dx, by+y1-TILE_BORDER*dy,
|
|
COL_WIRE);
|
|
} else {
|
|
draw_rect_coords(fe, 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)
|
|
{
|
|
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 (phase == 0) {
|
|
draw_rect(fe, bx+x1-X(dir), by+y1-Y(dir), w, h, COL_WIRE);
|
|
} else {
|
|
draw_rect(fe, bx+x1, by+y1, w, h, COL_BARRIER);
|
|
}
|
|
}
|
|
|
|
static void draw_tile(frontend *fe, game_state *state, int x, int y, int tile,
|
|
float angle, int cursor)
|
|
{
|
|
int bx = WINDOW_OFFSET + TILE_SIZE * x;
|
|
int by = WINDOW_OFFSET + TILE_SIZE * y;
|
|
float matrix[4];
|
|
float cx, cy, ex, ey, tx, ty;
|
|
int dir, col, phase;
|
|
|
|
/*
|
|
* 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.
|
|
*
|
|
* This would be terribly fiddly if we ever had to draw a tile
|
|
* while its neighbour was in mid-rotate, because we'd have to
|
|
* arrange to _know_ that the neighbour was being rotated and
|
|
* hence had an anomalous effect on the redraw of this tile.
|
|
* Fortunately, the drawing algorithm avoids ever calling us in
|
|
* this circumstance: we're either drawing lots of straight
|
|
* tiles at game start or after a move is complete, or we're
|
|
* repeatedly drawing only the rotating tile. So no problem.
|
|
*/
|
|
|
|
/*
|
|
* 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,
|
|
COL_BORDER);
|
|
draw_rect(fe, bx+TILE_BORDER, by+TILE_BORDER,
|
|
TILE_SIZE-TILE_BORDER, TILE_SIZE-TILE_BORDER,
|
|
tile & LOCKED ? COL_LOCKED : COL_BACKGROUND);
|
|
|
|
/*
|
|
* Draw an inset outline rectangle as a cursor, in whichever of
|
|
* COL_LOCKED and COL_BACKGROUND we aren't currently drawing
|
|
* in.
|
|
*/
|
|
if (cursor) {
|
|
draw_line(fe, 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,
|
|
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,
|
|
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,
|
|
bx+TILE_SIZE-TILE_SIZE/8, by+TILE_SIZE-TILE_SIZE/8,
|
|
tile & LOCKED ? COL_BACKGROUND : COL_LOCKED);
|
|
}
|
|
|
|
/*
|
|
* Set up the rotation matrix.
|
|
*/
|
|
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);
|
|
|
|
/*
|
|
* Draw the wires.
|
|
*/
|
|
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(fe, 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(fe, bx+(int)cx, by+(int)cy,
|
|
bx+(int)(cx+tx), by+(int)(cy+ty), 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.
|
|
*/
|
|
col = -1;
|
|
if (x == state->cx && y == state->cy)
|
|
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(fe, points, 4, TRUE, col);
|
|
draw_polygon(fe, points, 4, FALSE, COL_WIRE);
|
|
}
|
|
|
|
/*
|
|
* Draw the points on the border if other tiles are connected
|
|
* to us.
|
|
*/
|
|
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, ox, oy) & F(dir)))
|
|
continue;
|
|
|
|
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);
|
|
|
|
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(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);
|
|
} 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 barrier corners, and then barriers.
|
|
*/
|
|
for (phase = 0; phase < 2; phase++) {
|
|
for (dir = 1; dir < 0x10; dir <<= 1)
|
|
if (barrier(state, x, y) & (dir << 4))
|
|
draw_barrier_corner(fe, x, y, dir << 4, phase);
|
|
for (dir = 1; dir < 0x10; dir <<= 1)
|
|
if (barrier(state, x, y) & dir)
|
|
draw_barrier(fe, x, y, dir, phase);
|
|
}
|
|
|
|
draw_update(fe, 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)
|
|
{
|
|
int x, y, tx, ty, frame, last_rotate_dir;
|
|
unsigned char *active;
|
|
float angle = 0.0;
|
|
|
|
/*
|
|
* Clear the screen and draw the exterior barrier lines if this
|
|
* is our first call.
|
|
*/
|
|
if (!ds->started) {
|
|
int phase;
|
|
|
|
ds->started = TRUE;
|
|
|
|
draw_rect(fe, 0, 0,
|
|
WINDOW_OFFSET * 2 + TILE_SIZE * state->width + TILE_BORDER,
|
|
WINDOW_OFFSET * 2 + TILE_SIZE * state->height + TILE_BORDER,
|
|
COL_BACKGROUND);
|
|
draw_update(fe, 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 (barrier(state, x, 0) & UL)
|
|
draw_barrier_corner(fe, x, -1, LD, phase);
|
|
if (barrier(state, x, 0) & RU)
|
|
draw_barrier_corner(fe, x, -1, DR, phase);
|
|
if (barrier(state, x, 0) & U)
|
|
draw_barrier(fe, x, -1, D, phase);
|
|
if (barrier(state, x, ds->height-1) & DR)
|
|
draw_barrier_corner(fe, x, ds->height, RU, phase);
|
|
if (barrier(state, x, ds->height-1) & LD)
|
|
draw_barrier_corner(fe, x, ds->height, UL, phase);
|
|
if (barrier(state, x, ds->height-1) & D)
|
|
draw_barrier(fe, x, ds->height, U, phase);
|
|
}
|
|
|
|
for (y = 0; y < ds->height; y++) {
|
|
if (barrier(state, 0, y) & UL)
|
|
draw_barrier_corner(fe, -1, y, RU, phase);
|
|
if (barrier(state, 0, y) & LD)
|
|
draw_barrier_corner(fe, -1, y, DR, phase);
|
|
if (barrier(state, 0, y) & L)
|
|
draw_barrier(fe, -1, y, R, phase);
|
|
if (barrier(state, ds->width-1, y) & RU)
|
|
draw_barrier_corner(fe, ds->width, y, UL, phase);
|
|
if (barrier(state, ds->width-1, y) & DR)
|
|
draw_barrier_corner(fe, ds->width, y, LD, phase);
|
|
if (barrier(state, ds->width-1, y) & R)
|
|
draw_barrier(fe, ds->width, y, L, phase);
|
|
}
|
|
}
|
|
}
|
|
|
|
tx = ty = -1;
|
|
last_rotate_dir = dir==-1 ? oldstate->last_rotate_dir :
|
|
state->last_rotate_dir;
|
|
if (oldstate && (t < ROTATE_TIME) && last_rotate_dir) {
|
|
/*
|
|
* We're animating a single tile rotation. Find the turning
|
|
* tile.
|
|
*/
|
|
tx = (dir==-1 ? oldstate->last_rotate_x : state->last_rotate_x);
|
|
ty = (dir==-1 ? oldstate->last_rotate_y : state->last_rotate_y);
|
|
angle = last_rotate_dir * dir * 90.0F * (t / ROTATE_TIME);
|
|
state = oldstate;
|
|
}
|
|
|
|
frame = -1;
|
|
if (ft > 0) {
|
|
/*
|
|
* We're animating a completion flash. Find which frame
|
|
* we're at.
|
|
*/
|
|
frame = (int)(ft / FLASH_FRAME);
|
|
}
|
|
|
|
/*
|
|
* Draw any tile which differs from the way it was last drawn.
|
|
*/
|
|
active = compute_active(state);
|
|
|
|
for (x = 0; x < ds->width; x++)
|
|
for (y = 0; y < ds->height; y++) {
|
|
unsigned char c = tile(state, x, y) | index(state, active, x, y);
|
|
|
|
/*
|
|
* In a completion flash, we adjust the LOCKED bit
|
|
* depending on our distance from the centre point and
|
|
* the frame number.
|
|
*/
|
|
if (frame >= 0) {
|
|
int xdist, ydist, dist;
|
|
xdist = (x < state->cx ? state->cx - x : x - state->cx);
|
|
ydist = (y < state->cy ? state->cy - y : y - state->cy);
|
|
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 (index(state, ds->visible, x, y) != c ||
|
|
index(state, ds->visible, x, y) == 0xFF ||
|
|
(x == tx && y == ty) ||
|
|
(ui->cur_visible && x == ui->cur_x && y == ui->cur_y)) {
|
|
draw_tile(fe, state, x, y, c,
|
|
(x == tx && y == ty ? angle : 0.0F),
|
|
(ui->cur_visible && x == ui->cur_x && y == ui->cur_y));
|
|
if ((x == tx && y == ty) ||
|
|
(ui->cur_visible && x == ui->cur_x && y == ui->cur_y))
|
|
index(state, ds->visible, x, y) = 0xFF;
|
|
else
|
|
index(state, ds->visible, x, y) = c;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Update the status bar.
|
|
*/
|
|
{
|
|
char statusbuf[256];
|
|
int i, n, n2, a;
|
|
|
|
n = state->width * state->height;
|
|
for (i = a = n2 = 0; i < n; i++) {
|
|
if (active[i])
|
|
a++;
|
|
if (state->tiles[i] & 0xF)
|
|
n2++;
|
|
}
|
|
|
|
sprintf(statusbuf, "%sActive: %d/%d",
|
|
(state->used_solve ? "Auto-solved. " :
|
|
state->completed ? "COMPLETED! " : ""), a, n2);
|
|
|
|
status_bar(fe, statusbuf);
|
|
}
|
|
|
|
sfree(active);
|
|
}
|
|
|
|
static float game_anim_length(game_state *oldstate,
|
|
game_state *newstate, int dir)
|
|
{
|
|
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.
|
|
*/
|
|
last_rotate_dir = dir==-1 ? oldstate->last_rotate_dir :
|
|
newstate->last_rotate_dir;
|
|
if (last_rotate_dir)
|
|
return ROTATE_TIME;
|
|
|
|
return 0.0F;
|
|
}
|
|
|
|
static float game_flash_length(game_state *oldstate,
|
|
game_state *newstate, int dir)
|
|
{
|
|
/*
|
|
* If the game has just been completed, we display a completion
|
|
* flash.
|
|
*/
|
|
if (!oldstate->completed && newstate->completed &&
|
|
!oldstate->used_solve && !newstate->used_solve) {
|
|
int size;
|
|
size = 0;
|
|
if (size < newstate->cx+1)
|
|
size = newstate->cx+1;
|
|
if (size < newstate->cy+1)
|
|
size = newstate->cy+1;
|
|
if (size < newstate->width - newstate->cx)
|
|
size = newstate->width - newstate->cx;
|
|
if (size < newstate->height - newstate->cy)
|
|
size = newstate->height - newstate->cy;
|
|
return FLASH_FRAME * (size+4);
|
|
}
|
|
|
|
return 0.0F;
|
|
}
|
|
|
|
static int game_wants_statusbar(void)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
#ifdef COMBINED
|
|
#define thegame net
|
|
#endif
|
|
|
|
const struct game thegame = {
|
|
"Net", "games.net",
|
|
default_params,
|
|
game_fetch_preset,
|
|
decode_params,
|
|
encode_params,
|
|
free_params,
|
|
dup_params,
|
|
TRUE, game_configure, custom_params,
|
|
validate_params,
|
|
new_game_desc,
|
|
game_free_aux_info,
|
|
validate_desc,
|
|
new_game,
|
|
dup_game,
|
|
free_game,
|
|
TRUE, solve_game,
|
|
FALSE, game_text_format,
|
|
new_ui,
|
|
free_ui,
|
|
make_move,
|
|
game_size,
|
|
game_colours,
|
|
game_new_drawstate,
|
|
game_free_drawstate,
|
|
game_redraw,
|
|
game_anim_length,
|
|
game_flash_length,
|
|
game_wants_statusbar,
|
|
};
|