return ret;
}
-/*
- * The Mines game descriptions contain the location of every mine,
- * and can therefore be used to cheat.
- *
- * It would be pointless to attempt to _prevent_ this form of
- * cheating by encrypting the description, since Mines is
- * open-source so anyone can find out the encryption key. However,
- * I think it is worth doing a bit of gentle obfuscation to prevent
- * _accidental_ spoilers: if you happened to note that the game ID
- * starts with an F, for example, you might be unable to put the
- * knowledge of those mines out of your mind while playing. So,
- * just as discussions of film endings are rot13ed to avoid
- * spoiling it for people who don't want to be told, we apply a
- * keyless, reversible, but visually completely obfuscatory masking
- * function to the mine bitmap.
- */
-static void obfuscate_bitmap(unsigned char *bmp, int bits, int decode)
-{
- int bytes, firsthalf, secondhalf;
- struct step {
- unsigned char *seedstart;
- int seedlen;
- unsigned char *targetstart;
- int targetlen;
- } steps[2];
- int i, j;
-
- /*
- * My obfuscation algorithm is similar in concept to the OAEP
- * encoding used in some forms of RSA. Here's a specification
- * of it:
- *
- * + We have a `masking function' which constructs a stream of
- * pseudorandom bytes from a seed of some number of input
- * bytes.
- *
- * + We pad out our input bit stream to a whole number of
- * bytes by adding up to 7 zero bits on the end. (In fact
- * the bitmap passed as input to this function will already
- * have had this done in practice.)
- *
- * + We divide the _byte_ stream exactly in half, rounding the
- * half-way position _down_. So an 81-bit input string, for
- * example, rounds up to 88 bits or 11 bytes, and then
- * dividing by two gives 5 bytes in the first half and 6 in
- * the second half.
- *
- * + We generate a mask from the second half of the bytes, and
- * XOR it over the first half.
- *
- * + We generate a mask from the (encoded) first half of the
- * bytes, and XOR it over the second half. Any null bits at
- * the end which were added as padding are cleared back to
- * zero even if this operation would have made them nonzero.
- *
- * To de-obfuscate, the steps are precisely the same except
- * that the final two are reversed.
- *
- * Finally, our masking function. Given an input seed string of
- * bytes, the output mask consists of concatenating the SHA-1
- * hashes of the seed string and successive decimal integers,
- * starting from 0.
- */
-
- bytes = (bits + 7) / 8;
- firsthalf = bytes / 2;
- secondhalf = bytes - firsthalf;
-
- steps[decode ? 1 : 0].seedstart = bmp + firsthalf;
- steps[decode ? 1 : 0].seedlen = secondhalf;
- steps[decode ? 1 : 0].targetstart = bmp;
- steps[decode ? 1 : 0].targetlen = firsthalf;
-
- steps[decode ? 0 : 1].seedstart = bmp;
- steps[decode ? 0 : 1].seedlen = firsthalf;
- steps[decode ? 0 : 1].targetstart = bmp + firsthalf;
- steps[decode ? 0 : 1].targetlen = secondhalf;
-
- for (i = 0; i < 2; i++) {
- SHA_State base, final;
- unsigned char digest[20];
- char numberbuf[80];
- int digestpos = 20, counter = 0;
-
- SHA_Init(&base);
- SHA_Bytes(&base, steps[i].seedstart, steps[i].seedlen);
-
- for (j = 0; j < steps[i].targetlen; j++) {
- if (digestpos >= 20) {
- sprintf(numberbuf, "%d", counter++);
- final = base;
- SHA_Bytes(&final, numberbuf, strlen(numberbuf));
- SHA_Final(&final, digest);
- digestpos = 0;
- }
- steps[i].targetstart[j] ^= digest[digestpos++];
- }
-
- /*
- * Mask off the pad bits in the final byte after both steps.
- */
- if (bits % 8)
- bmp[bits / 8] &= 0xFF & (0xFF00 >> (bits % 8));
- }
-}
-
static char *describe_layout(char *grid, int area, int x, int y,
int obfuscate)
{
*/
ret = snewn((area+3)/4 + 100, char);
p = ret + sprintf(ret, "%d,%d,%s", x, y,
- obfuscate ? "m" : ""); /* 'm' == masked */
+ obfuscate ? "m" : "u"); /* 'm' == masked */
for (i = 0; i < (area+3)/4; i++) {
int v = bmp[i/2];
if (i % 2 == 0)
}
static char *new_game_desc(game_params *params, random_state *rs,
- game_aux_info **aux, int interactive)
+ char **aux, int interactive)
{
/*
* We generate the coordinates of an initial click even if they
}
}
-static void game_free_aux_info(game_aux_info *aux)
-{
- assert(!"Shouldn't happen");
-}
-
static char *validate_desc(game_params *params, char *desc)
{
int wh = params->w * params->h;
int x, y;
if (*desc == 'r') {
+ desc++;
if (!*desc || !isdigit((unsigned char)*desc))
return "No initial mine count in game description";
while (*desc && isdigit((unsigned char)*desc))
return "No ',' after uniqueness specifier in game description";
/* now ignore the rest */
} else {
- if (!*desc || !isdigit((unsigned char)*desc))
- return "No initial x-coordinate in game description";
- x = atoi(desc);
- if (x < 0 || x >= params->w)
- return "Initial x-coordinate was out of range";
- while (*desc && isdigit((unsigned char)*desc))
- desc++; /* skip over x coordinate */
- if (*desc != ',')
- return "No ',' after initial x-coordinate in game description";
- desc++; /* eat comma */
- if (!*desc || !isdigit((unsigned char)*desc))
- return "No initial y-coordinate in game description";
- y = atoi(desc);
- if (y < 0 || y >= params->h)
- return "Initial y-coordinate was out of range";
- while (*desc && isdigit((unsigned char)*desc))
- desc++; /* skip over y coordinate */
- if (*desc != ',')
- return "No ',' after initial y-coordinate in game description";
- desc++; /* eat comma */
- /* eat `m', meaning `masked', if present */
- if (*desc == 'm')
+ if (*desc && isdigit((unsigned char)*desc)) {
+ x = atoi(desc);
+ if (x < 0 || x >= params->w)
+ return "Initial x-coordinate was out of range";
+ while (*desc && isdigit((unsigned char)*desc))
+ desc++; /* skip over x coordinate */
+ if (*desc != ',')
+ return "No ',' after initial x-coordinate in game description";
+ desc++; /* eat comma */
+ if (!*desc || !isdigit((unsigned char)*desc))
+ return "No initial y-coordinate in game description";
+ y = atoi(desc);
+ if (y < 0 || y >= params->h)
+ return "Initial y-coordinate was out of range";
+ while (*desc && isdigit((unsigned char)*desc))
+ desc++; /* skip over y coordinate */
+ if (*desc != ',')
+ return "No ',' after initial y-coordinate in game description";
+ desc++; /* eat comma */
+ }
+ /* eat `m' for `masked' or `u' for `unmasked', if present */
+ if (*desc == 'm' || *desc == 'u')
desc++;
/* now just check length of remainder */
if (strlen(desc) != (wh+3)/4)
* hasn't been generated yet. Generate it based on the
* initial click location.
*/
- char *desc;
+ char *desc, *privdesc;
state->layout->mines = new_mine_layout(w, h, state->layout->n,
x, y, state->layout->unique,
state->layout->rs,
&desc);
- midend_supersede_game_desc(state->layout->me, desc);
+ /*
+ * Find the trailing substring of the game description
+ * corresponding to just the mine layout; we will use this
+ * as our second `private' game ID for serialisation.
+ */
+ privdesc = desc;
+ while (*privdesc && isdigit((unsigned char)*privdesc)) privdesc++;
+ if (*privdesc == ',') privdesc++;
+ while (*privdesc && isdigit((unsigned char)*privdesc)) privdesc++;
+ if (*privdesc == ',') privdesc++;
+ assert(*privdesc == 'm');
+ midend_supersede_game_desc(state->layout->me, desc, privdesc);
sfree(desc);
random_free(state->layout->rs);
state->layout->rs = NULL;
} else {
state->layout->rs = NULL;
state->layout->me = NULL;
-
state->layout->mines = snewn(wh, char);
- x = atoi(desc);
- while (*desc && isdigit((unsigned char)*desc))
- desc++; /* skip over x coordinate */
- if (*desc) desc++; /* eat comma */
- y = atoi(desc);
- while (*desc && isdigit((unsigned char)*desc))
- desc++; /* skip over y coordinate */
- if (*desc) desc++; /* eat comma */
+
+ if (*desc && isdigit((unsigned char)*desc)) {
+ x = atoi(desc);
+ while (*desc && isdigit((unsigned char)*desc))
+ desc++; /* skip over x coordinate */
+ if (*desc) desc++; /* eat comma */
+ y = atoi(desc);
+ while (*desc && isdigit((unsigned char)*desc))
+ desc++; /* skip over y coordinate */
+ if (*desc) desc++; /* eat comma */
+ } else {
+ x = y = -1;
+ }
if (*desc == 'm') {
masked = TRUE;
desc++;
} else {
+ if (*desc == 'u')
+ desc++;
/*
* We permit game IDs to be entered by hand without the
* masking transformation.
state->layout->mines[i] = 1;
}
- ret = open_square(state, x, y);
+ if (x >= 0 && y >= 0)
+ ret = open_square(state, x, y);
sfree(bmp);
}
sfree(state);
}
-static game_state *solve_game(game_state *state, game_state *currstate,
- game_aux_info *aux, char **error)
+static char *solve_game(game_state *state, game_state *currstate,
+ char *aux, char **error)
{
- /*
- * Simply expose the entire grid as if it were a completed
- * solution.
- */
- game_state *ret;
- int yy, xx;
-
if (!state->layout->mines) {
- *error = "Game has not been started yet";
- return NULL;
+ *error = "Game has not been started yet";
+ return NULL;
}
- ret = dup_game(state);
- for (yy = 0; yy < ret->h; yy++)
- for (xx = 0; xx < ret->w; xx++) {
-
- if (ret->layout->mines[yy*ret->w+xx]) {
- ret->grid[yy*ret->w+xx] = -1;
- } else {
- int dx, dy, v;
-
- v = 0;
-
- for (dx = -1; dx <= +1; dx++)
- for (dy = -1; dy <= +1; dy++)
- if (xx+dx >= 0 && xx+dx < ret->w &&
- yy+dy >= 0 && yy+dy < ret->h &&
- ret->layout->mines[(yy+dy)*ret->w+(xx+dx)])
- v++;
-
- ret->grid[yy*ret->w+xx] = v;
- }
- }
- ret->used_solve = ret->just_used_solve = TRUE;
- ret->won = TRUE;
-
- return ret;
+ return dupstr("S");
}
static char *game_text_format(game_state *state)
sfree(ui);
}
+static char *encode_ui(game_ui *ui)
+{
+ char buf[80];
+ /*
+ * The deaths counter needs preserving across a serialisation.
+ */
+ sprintf(buf, "D%d", ui->deaths);
+ return dupstr(buf);
+}
+
+static void decode_ui(game_ui *ui, char *encoding)
+{
+ sscanf(encoding, "D%d", &ui->deaths);
+}
+
static void game_changed_state(game_ui *ui, game_state *oldstate,
game_state *newstate)
{
*/
};
-static game_state *make_move(game_state *from, game_ui *ui, game_drawstate *ds,
- int x, int y, int button)
+static char *interpret_move(game_state *from, game_ui *ui, game_drawstate *ds,
+ int x, int y, int button)
{
- game_state *ret;
int cx, cy;
+ char buf[256];
if (from->dead || from->won)
return NULL; /* no further moves permitted */
ui->hx = cx;
ui->hy = cy;
ui->hradius = (from->grid[cy*from->w+cx] >= 0 ? 1 : 0);
- return from;
+ return "";
}
if (button == RIGHT_BUTTON) {
from->grid[cy * from->w + cx] != -1)
return NULL;
- ret = dup_game(from);
- ret->just_used_solve = FALSE;
- ret->grid[cy * from->w + cx] ^= (-2 ^ -1);
-
- return ret;
+ sprintf(buf, "F%d,%d", cx, cy);
+ return dupstr(buf);
}
if (button == LEFT_RELEASE || button == MIDDLE_RELEASE) {
/*
* At this stage we must never return NULL: we have adjusted
- * the ui, so at worst we return `from'.
+ * the ui, so at worst we return "".
*/
if (cx < 0 || cx >= from->w || cy < 0 || cy >= from->h)
- return from;
+ return "";
/*
* Left-clicking on a covered square opens a tile. Not
if (button == LEFT_RELEASE &&
(from->grid[cy * from->w + cx] == -2 ||
from->grid[cy * from->w + cx] == -3)) {
- ret = dup_game(from);
- ret->just_used_solve = FALSE;
- open_square(ret, cx, cy);
- if (ret->dead)
- ui->deaths++;
- return ret;
+ /* Check if you've killed yourself. */
+ if (from->layout->mines && from->layout->mines[cy * from->w + cx])
+ ui->deaths++;
+
+ sprintf(buf, "O%d,%d", cx, cy);
+ return dupstr(buf);
}
/*
}
if (n == from->grid[cy * from->w + cx]) {
- ret = dup_game(from);
- ret->just_used_solve = FALSE;
+
+ /*
+ * Now see if any of the squares we're clearing
+ * contains a mine (which will happen iff you've
+ * incorrectly marked the mines around the clicked
+ * square). If so, we open _just_ those squares, to
+ * reveal as little additional information as we
+ * can.
+ */
+ char *p = buf;
+ char *sep = "";
+
+ for (dy = -1; dy <= +1; dy++)
+ for (dx = -1; dx <= +1; dx++)
+ if (cx+dx >= 0 && cx+dx < from->w &&
+ cy+dy >= 0 && cy+dy < from->h) {
+ if (from->grid[(cy+dy)*from->w+(cx+dx)] != -1 &&
+ from->layout->mines &&
+ from->layout->mines[(cy+dy)*from->w+(cx+dx)]) {
+ p += sprintf(p, "%sO%d,%d", sep, cx+dx, cy+dy);
+ sep = ";";
+ }
+ }
+
+ if (p > buf) {
+ ui->deaths++;
+ } else {
+ sprintf(buf, "C%d,%d", cx, cy);
+ }
+
+ return dupstr(buf);
+ }
+ }
+
+ return "";
+ }
+
+ return NULL;
+}
+
+static game_state *execute_move(game_state *from, char *move)
+{
+ int cy, cx;
+ game_state *ret;
+
+ if (!strcmp(move, "S")) {
+ /*
+ * Simply expose the entire grid as if it were a completed
+ * solution.
+ */
+ int yy, xx;
+
+ ret = dup_game(from);
+ for (yy = 0; yy < ret->h; yy++)
+ for (xx = 0; xx < ret->w; xx++) {
+
+ if (ret->layout->mines[yy*ret->w+xx]) {
+ ret->grid[yy*ret->w+xx] = -1;
+ } else {
+ int dx, dy, v;
+
+ v = 0;
+
+ for (dx = -1; dx <= +1; dx++)
+ for (dy = -1; dy <= +1; dy++)
+ if (xx+dx >= 0 && xx+dx < ret->w &&
+ yy+dy >= 0 && yy+dy < ret->h &&
+ ret->layout->mines[(yy+dy)*ret->w+(xx+dx)])
+ v++;
+
+ ret->grid[yy*ret->w+xx] = v;
+ }
+ }
+ ret->used_solve = ret->just_used_solve = TRUE;
+ ret->won = TRUE;
+
+ return ret;
+ } else {
+ ret = dup_game(from);
+ ret->just_used_solve = FALSE;
+
+ while (*move) {
+ if (move[0] == 'F' &&
+ sscanf(move+1, "%d,%d", &cx, &cy) == 2 &&
+ cx >= 0 && cx < from->w && cy >= 0 && cy < from->h) {
+ ret->grid[cy * from->w + cx] ^= (-2 ^ -1);
+ } else if (move[0] == 'O' &&
+ sscanf(move+1, "%d,%d", &cx, &cy) == 2 &&
+ cx >= 0 && cx < from->w && cy >= 0 && cy < from->h) {
+ open_square(ret, cx, cy);
+ } else if (move[0] == 'C' &&
+ sscanf(move+1, "%d,%d", &cx, &cy) == 2 &&
+ cx >= 0 && cx < from->w && cy >= 0 && cy < from->h) {
+ int dx, dy;
+
for (dy = -1; dy <= +1; dy++)
for (dx = -1; dx <= +1; dx++)
if (cx+dx >= 0 && cx+dx < ret->w &&
(ret->grid[(cy+dy)*ret->w+(cx+dx)] == -2 ||
ret->grid[(cy+dy)*ret->w+(cx+dx)] == -3))
open_square(ret, cx+dx, cy+dy);
- if (ret->dead)
- ui->deaths++;
- return ret;
+ } else {
+ free_game(ret);
+ return NULL;
}
+
+ while (*move && *move != ';') move++;
+ if (*move) move++;
}
- return from;
+ return ret;
}
-
- return NULL;
}
/* ----------------------------------------------------------------------
static void game_size(game_params *params, game_drawstate *ds,
int *x, int *y, int expand)
{
- int tsx, tsy, ts;
+ double tsx, tsy, ts;
/*
* Each window dimension equals the tile size times 3 more than
* the grid dimension (the border is 3/2 the width of the
* tiles).
*/
- tsx = *x / (params->w + 3);
- tsy = *y / (params->h + 3);
+ tsx = (double)*x / ((double)params->w + 3.0);
+ tsy = (double)*y / ((double)params->h + 3.0);
ts = min(tsx, tsy);
if (expand)
- ds->tilesize = ts;
+ ds->tilesize = (int)(ts + 0.5);
else
- ds->tilesize = min(ts, PREFERRED_TILE_SIZE);
+ ds->tilesize = min((int)ts, PREFERRED_TILE_SIZE);
*x = BORDER * 2 + TILE_SIZE * params->w;
*y = BORDER * 2 + TILE_SIZE * params->h;
TRUE, game_configure, custom_params,
validate_params,
new_game_desc,
- game_free_aux_info,
validate_desc,
new_game,
dup_game,
TRUE, game_text_format,
new_ui,
free_ui,
+ encode_ui,
+ decode_ui,
game_changed_state,
- make_move,
+ interpret_move,
+ execute_move,
game_size,
game_colours,
game_new_drawstate,