mirror of
git://git.tartarus.org/simon/puzzles.git
synced 2025-04-21 08:01:30 -07:00
Files

I don't know how I've never thought of this before! Pretty much every game in this collection has to have a mechanism for noticing when game_redraw is called for the first time on a new drawstate, and if so, start by covering the whole window with a filled rectangle of the background colour. This is a pain for implementers, and also awkward because the drawstate often has to _work out_ its own pixel size (or else remember it from when its size method was called). The backends all do that so that the frontends don't have to guarantee anything about the initial window contents. But that's a silly tradeoff to begin with (there are way more backends than frontends, so this _adds_ work rather than saving it), and also, in this code base there's a standard way to handle things you don't want to have to do in every backend _or_ every frontend: do them just once in the midend! So now that rectangle-drawing operation happens in midend_redraw, and I've been able to remove it from almost every puzzle. (A couple of puzzles have other approaches: Slant didn't have a rectangle-draw because it handles even the game borders using its per-tile redraw function, and Untangle clears the whole window on every redraw _anyway_ because it would just be too confusing not to.) In some cases I've also been able to remove the 'started' flag from the drawstate. But in many cases that has to stay because it also triggers drawing of static display furniture other than the background.
1478 lines
40 KiB
C
1478 lines
40 KiB
C
/*
|
|
* sokoban.c: An implementation of the well-known Sokoban barrel-
|
|
* pushing game. Random generation is too simplistic to be
|
|
* credible, but the rest of the gameplay works well enough to use
|
|
* it with hand-written level descriptions.
|
|
*/
|
|
|
|
/*
|
|
* TODO:
|
|
*
|
|
* - I think it would be better to ditch the `prev' array, and
|
|
* instead make the `dist' array strictly monotonic (by having
|
|
* each distance be something like I*A+S, where A is the grid
|
|
* area, I the number of INITIAL squares trampled on, and S the
|
|
* number of harmless spaces moved through). This would permit
|
|
* the path-tracing when a pull is actually made to choose
|
|
* randomly from all the possible shortest routes, which would
|
|
* be superior in terms of eliminating directional bias.
|
|
* + So when tracing the path back to the current px,py, we
|
|
* look at all four adjacent squares, find the minimum
|
|
* distance, check that it's _strictly smaller_ than that of
|
|
* the current square, and restrict our choice to precisely
|
|
* those squares with that minimum distance.
|
|
* + The other place `prev' is currently used is in the check
|
|
* for consistency of a pull. We would have to replace the
|
|
* check for whether prev[ny*w+nx]==oy*w+ox with a check that
|
|
* made sure there was at least one adjacent square with a
|
|
* smaller distance which _wasn't_ oy*w+ox. Then when we did
|
|
* the path-tracing we'd also have to take this special case
|
|
* into account.
|
|
*
|
|
* - More discriminating choice of pull. (Snigger.)
|
|
* + favour putting targets in clumps
|
|
* + try to shoot for a reasonably consistent number of barrels
|
|
* (adjust willingness to generate a new barrel depending on
|
|
* how many are already present)
|
|
* + adjust willingness to break new ground depending on how
|
|
* much is already broken
|
|
*
|
|
* - generation time parameters:
|
|
* + enable NetHack mode (and find a better place for the hole)
|
|
* + decide how many of the remaining Is should be walls
|
|
*
|
|
* - at the end of generation, randomly position the starting
|
|
* player coordinates, probably by (somehow) reusing the same
|
|
* bfs currently inside the loop.
|
|
*
|
|
* - possible backtracking?
|
|
*
|
|
* - IWBNI we could spot completely unreachable bits of level at
|
|
* the outside, and not bother drawing grid lines for them. The
|
|
* NH levels currently look a bit weird with grid lines on the
|
|
* outside of the boundary.
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <math.h>
|
|
|
|
#include "puzzles.h"
|
|
|
|
/*
|
|
* Various subsets of these constants are used during game
|
|
* generation, game play, game IDs and the game_drawstate.
|
|
*/
|
|
#define INITIAL 'i' /* used only in game generation */
|
|
#define SPACE 's'
|
|
#define WALL 'w'
|
|
#define PIT 'p'
|
|
#define DEEP_PIT 'd'
|
|
#define TARGET 't'
|
|
#define BARREL 'b'
|
|
#define BARRELTARGET 'f' /* target is 'f'illed */
|
|
#define PLAYER 'u' /* yo'u'; used in game IDs */
|
|
#define PLAYERTARGET 'v' /* bad letter: v is to u as t is to s */
|
|
#define INVALID '!' /* used in drawstate to force redraw */
|
|
/*
|
|
* We also support the use of any capital letter as a barrel, which
|
|
* will be displayed with that letter as a label. (This facilitates
|
|
* people distributing annotated game IDs for particular Sokoban
|
|
* levels, so they can accompany them with verbal instructions
|
|
* about pushing particular barrels in particular ways.) Therefore,
|
|
* to find out whether something is a barrel, we need a test
|
|
* function which does a bit more than just comparing to BARREL.
|
|
*
|
|
* When resting on target squares, capital-letter barrels are
|
|
* replaced with their control-character value (A -> ^A).
|
|
*/
|
|
#define IS_PLAYER(c) ( (c)==PLAYER || (c)==PLAYERTARGET )
|
|
#define IS_BARREL(c) ( (c)==BARREL || (c)==BARRELTARGET || \
|
|
((c)>='A' && (c)<='Z') || ((c)>=1 && (c)<=26) )
|
|
#define IS_ON_TARGET(c) ( (c)==TARGET || (c)==BARRELTARGET || \
|
|
(c)==PLAYERTARGET || ((c)>=1 && (c)<=26) )
|
|
#define TARGETISE(b) ( (b)==BARREL ? BARRELTARGET : (b)-('A'-1) )
|
|
#define DETARGETISE(b) ( (b)==BARRELTARGET ? BARREL : (b)+('A'-1) )
|
|
#define BARREL_LABEL(b) ( (b)>='A'&&(b)<='Z' ? (b) : \
|
|
(b)>=1 && (b)<=26 ? (b)+('A'-1) : 0 )
|
|
|
|
#define DX(d) (d == 0 ? -1 : d == 2 ? +1 : 0)
|
|
#define DY(d) (d == 1 ? -1 : d == 3 ? +1 : 0)
|
|
|
|
#define FLASH_LENGTH 0.3F
|
|
|
|
enum {
|
|
COL_BACKGROUND,
|
|
COL_TARGET,
|
|
COL_PIT,
|
|
COL_DEEP_PIT,
|
|
COL_BARREL,
|
|
COL_PLAYER,
|
|
COL_TEXT,
|
|
COL_GRID,
|
|
COL_OUTLINE,
|
|
COL_HIGHLIGHT,
|
|
COL_LOWLIGHT,
|
|
COL_WALL,
|
|
NCOLOURS
|
|
};
|
|
|
|
struct game_params {
|
|
int w, h;
|
|
/*
|
|
* FIXME: a parameter involving degree of filling in?
|
|
*/
|
|
};
|
|
|
|
struct game_state {
|
|
game_params p;
|
|
unsigned char *grid;
|
|
int px, py;
|
|
bool completed;
|
|
};
|
|
|
|
static game_params *default_params(void)
|
|
{
|
|
game_params *ret = snew(game_params);
|
|
|
|
ret->w = 12;
|
|
ret->h = 10;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void free_params(game_params *params)
|
|
{
|
|
sfree(params);
|
|
}
|
|
|
|
static game_params *dup_params(const game_params *params)
|
|
{
|
|
game_params *ret = snew(game_params);
|
|
*ret = *params; /* structure copy */
|
|
return ret;
|
|
}
|
|
|
|
static const struct game_params sokoban_presets[] = {
|
|
{ 12, 10 },
|
|
{ 16, 12 },
|
|
{ 20, 16 },
|
|
};
|
|
|
|
static bool game_fetch_preset(int i, char **name, game_params **params)
|
|
{
|
|
game_params p, *ret;
|
|
char *retname;
|
|
char namebuf[80];
|
|
|
|
if (i < 0 || i >= lenof(sokoban_presets))
|
|
return false;
|
|
|
|
p = sokoban_presets[i];
|
|
ret = dup_params(&p);
|
|
sprintf(namebuf, "%dx%d", ret->w, ret->h);
|
|
retname = dupstr(namebuf);
|
|
|
|
*params = ret;
|
|
*name = retname;
|
|
return true;
|
|
}
|
|
|
|
static void decode_params(game_params *params, char const *string)
|
|
{
|
|
params->w = params->h = atoi(string);
|
|
while (*string && isdigit((unsigned char)*string)) string++;
|
|
if (*string == 'x') {
|
|
string++;
|
|
params->h = atoi(string);
|
|
}
|
|
}
|
|
|
|
static char *encode_params(const game_params *params, bool full)
|
|
{
|
|
char data[256];
|
|
|
|
sprintf(data, "%dx%d", params->w, params->h);
|
|
|
|
return dupstr(data);
|
|
}
|
|
|
|
static config_item *game_configure(const game_params *params)
|
|
{
|
|
config_item *ret;
|
|
char buf[80];
|
|
|
|
ret = snewn(3, config_item);
|
|
|
|
ret[0].name = "Width";
|
|
ret[0].type = C_STRING;
|
|
sprintf(buf, "%d", params->w);
|
|
ret[0].u.string.sval = dupstr(buf);
|
|
|
|
ret[1].name = "Height";
|
|
ret[1].type = C_STRING;
|
|
sprintf(buf, "%d", params->h);
|
|
ret[1].u.string.sval = dupstr(buf);
|
|
|
|
ret[2].name = NULL;
|
|
ret[2].type = C_END;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static game_params *custom_params(const config_item *cfg)
|
|
{
|
|
game_params *ret = snew(game_params);
|
|
|
|
ret->w = atoi(cfg[0].u.string.sval);
|
|
ret->h = atoi(cfg[1].u.string.sval);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static const char *validate_params(const game_params *params, bool full)
|
|
{
|
|
if (params->w < 4 || params->h < 4)
|
|
return "Width and height must both be at least 4";
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
* Game generation mechanism.
|
|
*
|
|
* To generate a Sokoban level, we begin with a completely blank
|
|
* grid and make valid inverse moves. Grid squares can be in a
|
|
* number of states. The states are:
|
|
*
|
|
* - INITIAL: this square has not as yet been touched by any
|
|
* inverse move, which essentially means we haven't decided what
|
|
* it is yet.
|
|
*
|
|
* - SPACE: this square is a space.
|
|
*
|
|
* - TARGET: this square is a space which is also the target for a
|
|
* barrel.
|
|
*
|
|
* - BARREL: this square contains a barrel.
|
|
*
|
|
* - BARRELTARGET: this square contains a barrel _on_ a target.
|
|
*
|
|
* - WALL: this square is a wall.
|
|
*
|
|
* - PLAYER: this square contains the player.
|
|
*
|
|
* - PLAYERTARGET: this square contains the player on a target.
|
|
*
|
|
* We begin with every square of the in state INITIAL, apart from a
|
|
* solid ring of WALLs around the edge. We randomly position the
|
|
* PLAYER somewhere. Thereafter our valid moves are:
|
|
*
|
|
* - to move the PLAYER in one direction _pulling_ a barrel after
|
|
* us. For this to work, we must have SPACE or INITIAL in the
|
|
* direction we're moving, and BARREL or BARRELTARGET in the
|
|
* direction we're moving away from. We leave SPACE or TARGET
|
|
* respectively in the vacated square.
|
|
*
|
|
* - to create a new barrel by transforming an INITIAL square into
|
|
* BARRELTARGET.
|
|
*
|
|
* - to move the PLAYER freely through SPACE and TARGET squares,
|
|
* leaving SPACE or TARGET where it started.
|
|
*
|
|
* - to move the player through INITIAL squares, carving a tunnel
|
|
* of SPACEs as it goes.
|
|
*
|
|
* We try to avoid destroying INITIAL squares wherever possible (if
|
|
* there's a path to where we want to be using only SPACE, then we
|
|
* should always use that). At the end of generation, every square
|
|
* still in state INITIAL is one which was not required at any
|
|
* point during generation, which means we can randomly choose
|
|
* whether to make it SPACE or WALL.
|
|
*
|
|
* It's unclear as yet what the right strategy for wall placement
|
|
* should be. Too few WALLs will yield many alternative solutions
|
|
* to the puzzle, whereas too many might rule out so many
|
|
* possibilities that the intended solution becomes obvious.
|
|
*/
|
|
|
|
static void sokoban_generate(int w, int h, unsigned char *grid, int moves,
|
|
bool nethack, random_state *rs)
|
|
{
|
|
struct pull {
|
|
int ox, oy, nx, ny, score;
|
|
};
|
|
|
|
struct pull *pulls;
|
|
int *dist, *prev, *heap;
|
|
int x, y, px, py, i, j, d, heapsize, npulls;
|
|
|
|
pulls = snewn(w * h * 4, struct pull);
|
|
dist = snewn(w * h, int);
|
|
prev = snewn(w * h, int);
|
|
heap = snewn(w * h, int);
|
|
|
|
/*
|
|
* Configure the initial grid.
|
|
*/
|
|
for (y = 0; y < h; y++)
|
|
for (x = 0; x < w; x++)
|
|
grid[y*w+x] = (x == 0 || y == 0 || x == w-1 || y == h-1 ?
|
|
WALL : INITIAL);
|
|
if (nethack)
|
|
grid[1] = DEEP_PIT;
|
|
|
|
/*
|
|
* Place the player.
|
|
*/
|
|
i = random_upto(rs, (w-2) * (h-2));
|
|
x = 1 + i % (w-2);
|
|
y = 1 + i / (w-2);
|
|
grid[y*w+x] = SPACE;
|
|
px = x;
|
|
py = y;
|
|
|
|
/*
|
|
* Now loop around making random inverse Sokoban moves. In this
|
|
* loop we aim to make one actual barrel-pull per iteration,
|
|
* plus as many free moves as are necessary to get into
|
|
* position for that pull.
|
|
*/
|
|
while (moves-- >= 0) {
|
|
/*
|
|
* First enumerate all the viable barrel-pulls we can
|
|
* possibly make, counting two pulls of the same barrel in
|
|
* different directions as different. We also include pulls
|
|
* we can perform by creating a new barrel. Each pull is
|
|
* marked with the amount of violence it would have to do
|
|
* to the grid.
|
|
*/
|
|
npulls = 0;
|
|
for (y = 0; y < h; y++)
|
|
for (x = 0; x < w; x++)
|
|
for (d = 0; d < 4; d++) {
|
|
int dx = DX(d);
|
|
int dy = DY(d);
|
|
int nx = x + dx, ny = y + dy;
|
|
int npx = nx + dx, npy = ny + dy;
|
|
int score = 0;
|
|
|
|
/*
|
|
* The candidate move is to put the player at
|
|
* (nx,ny), and move him to (npx,npy), pulling
|
|
* a barrel at (x,y) to (nx,ny). So first we
|
|
* must check that all those squares are within
|
|
* the boundaries of the grid. For this it is
|
|
* sufficient to check npx,npy.
|
|
*/
|
|
if (npx < 0 || npx >= w || npy < 0 || npy >= h)
|
|
continue;
|
|
|
|
/*
|
|
* (x,y) must either be a barrel, or a square
|
|
* which we can convert into a barrel.
|
|
*/
|
|
switch (grid[y*w+x]) {
|
|
case BARREL: case BARRELTARGET:
|
|
break;
|
|
case INITIAL:
|
|
if (nethack)
|
|
continue;
|
|
score += 10 /* new_barrel_score */;
|
|
break;
|
|
case DEEP_PIT:
|
|
if (!nethack)
|
|
continue;
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* (nx,ny) must either be a space, or a square
|
|
* which we can convert into a space.
|
|
*/
|
|
switch (grid[ny*w+nx]) {
|
|
case SPACE: case TARGET:
|
|
break;
|
|
case INITIAL:
|
|
score += 3 /* new_space_score */;
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* (npx,npy) must also either be a space, or a
|
|
* square which we can convert into a space.
|
|
*/
|
|
switch (grid[npy*w+npx]) {
|
|
case SPACE: case TARGET:
|
|
break;
|
|
case INITIAL:
|
|
score += 3 /* new_space_score */;
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* That's sufficient to tag this as a possible
|
|
* pull right now. We still don't know if we
|
|
* can reach the required player position, but
|
|
* that's a job for the subsequent BFS phase to
|
|
* tell us.
|
|
*/
|
|
pulls[npulls].ox = x;
|
|
pulls[npulls].oy = y;
|
|
pulls[npulls].nx = nx;
|
|
pulls[npulls].ny = ny;
|
|
pulls[npulls].score = score;
|
|
#ifdef GENERATION_DIAGNOSTICS
|
|
printf("found potential pull: (%d,%d)-(%d,%d) cost %d\n",
|
|
pulls[npulls].ox, pulls[npulls].oy,
|
|
pulls[npulls].nx, pulls[npulls].ny,
|
|
pulls[npulls].score);
|
|
#endif
|
|
npulls++;
|
|
}
|
|
#ifdef GENERATION_DIAGNOSTICS
|
|
printf("found %d potential pulls\n", npulls);
|
|
#endif
|
|
|
|
/*
|
|
* If there are no pulls available at all, we give up.
|
|
*
|
|
* (FIXME: or perhaps backtrack?)
|
|
*/
|
|
if (npulls == 0)
|
|
break;
|
|
|
|
/*
|
|
* Now we do a BFS from our current position, to find all
|
|
* the squares we can get the player into.
|
|
*
|
|
* This BFS is unusually tricky. We want to give a positive
|
|
* distance only to squares which we have to carve through
|
|
* INITIALs to get to, which means we can't just stick
|
|
* every square we reach on the end of our to-do list.
|
|
* Instead, we must maintain our list as a proper priority
|
|
* queue.
|
|
*/
|
|
for (i = 0; i < w*h; i++)
|
|
dist[i] = prev[i] = -1;
|
|
|
|
heap[0] = py*w+px;
|
|
heapsize = 1;
|
|
dist[py*w+px] = 0;
|
|
|
|
#define PARENT(n) ( ((n)-1)/2 )
|
|
#define LCHILD(n) ( 2*(n)+1 )
|
|
#define RCHILD(n) ( 2*(n)+2 )
|
|
#define SWAP(i,j) do { int swaptmp = (i); (i) = (j); (j) = swaptmp; } while (0)
|
|
|
|
while (heapsize > 0) {
|
|
/*
|
|
* Pull the smallest element off the heap: it's at
|
|
* position 0. Move the arbitrary element from the very
|
|
* end of the heap into position 0.
|
|
*/
|
|
y = heap[0] / w;
|
|
x = heap[0] % w;
|
|
|
|
heapsize--;
|
|
heap[0] = heap[heapsize];
|
|
|
|
/*
|
|
* Now repeatedly move that arbitrary element down the
|
|
* heap by swapping it with the more suitable of its
|
|
* children.
|
|
*/
|
|
i = 0;
|
|
while (1) {
|
|
int lc, rc;
|
|
|
|
lc = LCHILD(i);
|
|
rc = RCHILD(i);
|
|
|
|
if (lc >= heapsize)
|
|
break; /* we've hit bottom */
|
|
|
|
if (rc >= heapsize) {
|
|
/*
|
|
* Special case: there is only one child to
|
|
* check.
|
|
*/
|
|
if (dist[heap[i]] > dist[heap[lc]])
|
|
SWAP(heap[i], heap[lc]);
|
|
|
|
/* _Now_ we've hit bottom. */
|
|
break;
|
|
} else {
|
|
/*
|
|
* The common case: there are two children and
|
|
* we must check them both.
|
|
*/
|
|
if (dist[heap[i]] > dist[heap[lc]] ||
|
|
dist[heap[i]] > dist[heap[rc]]) {
|
|
/*
|
|
* Pick the more appropriate child to swap with
|
|
* (i.e. the one which would want to be the
|
|
* parent if one were above the other - as one
|
|
* is about to be).
|
|
*/
|
|
if (dist[heap[lc]] > dist[heap[rc]]) {
|
|
SWAP(heap[i], heap[rc]);
|
|
i = rc;
|
|
} else {
|
|
SWAP(heap[i], heap[lc]);
|
|
i = lc;
|
|
}
|
|
} else {
|
|
/* This element is in the right place; we're done. */
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* OK, that's given us (x,y) for this phase of the
|
|
* search. Now try all directions from here.
|
|
*/
|
|
|
|
for (d = 0; d < 4; d++) {
|
|
int dx = DX(d);
|
|
int dy = DY(d);
|
|
int nx = x + dx, ny = y + dy;
|
|
if (nx < 0 || nx >= w || ny < 0 || ny >= h)
|
|
continue;
|
|
if (grid[ny*w+nx] != SPACE && grid[ny*w+nx] != TARGET &&
|
|
grid[ny*w+nx] != INITIAL)
|
|
continue;
|
|
if (dist[ny*w+nx] == -1) {
|
|
dist[ny*w+nx] = dist[y*w+x] + (grid[ny*w+nx] == INITIAL);
|
|
prev[ny*w+nx] = y*w+x;
|
|
|
|
/*
|
|
* Now insert ny*w+nx at the end of the heap,
|
|
* and move it down to its appropriate resting
|
|
* place.
|
|
*/
|
|
i = heapsize;
|
|
heap[heapsize++] = ny*w+nx;
|
|
|
|
/*
|
|
* Swap element n with its parent repeatedly to
|
|
* preserve the heap property.
|
|
*/
|
|
|
|
while (i > 0) {
|
|
int p = PARENT(i);
|
|
|
|
if (dist[heap[p]] > dist[heap[i]]) {
|
|
SWAP(heap[p], heap[i]);
|
|
i = p;
|
|
} else
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#undef PARENT
|
|
#undef LCHILD
|
|
#undef RCHILD
|
|
#undef SWAP
|
|
|
|
#ifdef GENERATION_DIAGNOSTICS
|
|
printf("distance map:\n");
|
|
for (i = 0; i < h; i++) {
|
|
for (j = 0; j < w; j++) {
|
|
int d = dist[i*w+j];
|
|
int c;
|
|
if (d < 0)
|
|
c = '#';
|
|
else if (d >= 36)
|
|
c = '!';
|
|
else if (d >= 10)
|
|
c = 'A' - 10 + d;
|
|
else
|
|
c = '0' + d;
|
|
putchar(c);
|
|
}
|
|
putchar('\n');
|
|
}
|
|
#endif
|
|
|
|
/*
|
|
* Now we can go back through the `pulls' array, adjusting
|
|
* the score for each pull depending on how hard it is to
|
|
* reach its starting point, and also throwing out any
|
|
* whose starting points are genuinely unreachable even
|
|
* with the possibility of carving through INITIAL squares.
|
|
*/
|
|
for (i = j = 0; i < npulls; i++) {
|
|
#ifdef GENERATION_DIAGNOSTICS
|
|
printf("potential pull (%d,%d)-(%d,%d)",
|
|
pulls[i].ox, pulls[i].oy,
|
|
pulls[i].nx, pulls[i].ny);
|
|
#endif
|
|
x = pulls[i].nx;
|
|
y = pulls[i].ny;
|
|
if (dist[y*w+x] < 0) {
|
|
#ifdef GENERATION_DIAGNOSTICS
|
|
printf(" unreachable\n");
|
|
#endif
|
|
continue; /* this pull isn't feasible at all */
|
|
} else {
|
|
/*
|
|
* Another nasty special case we have to check is
|
|
* whether the initial barrel location (ox,oy) is
|
|
* on the path used to reach the square. This can
|
|
* occur if that square is in state INITIAL: the
|
|
* pull is initially considered valid on the basis
|
|
* that the INITIAL can become BARRELTARGET, and
|
|
* it's also considered reachable on the basis that
|
|
* INITIAL can be turned into SPACE, but it can't
|
|
* be both at once.
|
|
*
|
|
* Fortunately, if (ox,oy) is on the path at all,
|
|
* it must be only one space from the end, so this
|
|
* is easy to spot and rule out.
|
|
*/
|
|
if (prev[y*w+x] == pulls[i].oy*w+pulls[i].ox) {
|
|
#ifdef GENERATION_DIAGNOSTICS
|
|
printf(" goes through itself\n");
|
|
#endif
|
|
continue; /* this pull isn't feasible at all */
|
|
}
|
|
pulls[j] = pulls[i]; /* structure copy */
|
|
pulls[j].score += dist[y*w+x] * 3 /* new_space_score */;
|
|
#ifdef GENERATION_DIAGNOSTICS
|
|
printf(" reachable at distance %d (cost now %d)\n",
|
|
dist[y*w+x], pulls[j].score);
|
|
#endif
|
|
j++;
|
|
}
|
|
}
|
|
npulls = j;
|
|
|
|
/*
|
|
* Again, if there are no pulls available at all, we give
|
|
* up.
|
|
*
|
|
* (FIXME: or perhaps backtrack?)
|
|
*/
|
|
if (npulls == 0)
|
|
break;
|
|
|
|
/*
|
|
* Now choose which pull to make. On the one hand we should
|
|
* prefer pulls which do less damage to the INITIAL squares
|
|
* (thus, ones for which we can already get into position
|
|
* via existing SPACEs, and for which the barrel already
|
|
* exists and doesn't have to be invented); on the other,
|
|
* we want to avoid _always_ preferring such pulls, on the
|
|
* grounds that that will lead to levels without very much
|
|
* stuff in.
|
|
*
|
|
* When creating new barrels, we prefer creations which are
|
|
* next to existing TARGET squares.
|
|
*
|
|
* FIXME: for the moment I'll make this very simple indeed.
|
|
*/
|
|
i = random_upto(rs, npulls);
|
|
|
|
/*
|
|
* Actually make the pull, including carving a path to get
|
|
* to the site if necessary.
|
|
*/
|
|
x = pulls[i].nx;
|
|
y = pulls[i].ny;
|
|
while (prev[y*w+x] >= 0) {
|
|
int p;
|
|
|
|
if (grid[y*w+x] == INITIAL)
|
|
grid[y*w+x] = SPACE;
|
|
|
|
p = prev[y*w+x];
|
|
y = p / w;
|
|
x = p % w;
|
|
}
|
|
px = 2*pulls[i].nx - pulls[i].ox;
|
|
py = 2*pulls[i].ny - pulls[i].oy;
|
|
if (grid[py*w+px] == INITIAL)
|
|
grid[py*w+px] = SPACE;
|
|
if (grid[pulls[i].ny*w+pulls[i].nx] == TARGET)
|
|
grid[pulls[i].ny*w+pulls[i].nx] = BARRELTARGET;
|
|
else
|
|
grid[pulls[i].ny*w+pulls[i].nx] = BARREL;
|
|
if (grid[pulls[i].oy*w+pulls[i].ox] == BARREL)
|
|
grid[pulls[i].oy*w+pulls[i].ox] = SPACE;
|
|
else if (grid[pulls[i].oy*w+pulls[i].ox] != DEEP_PIT)
|
|
grid[pulls[i].oy*w+pulls[i].ox] = TARGET;
|
|
}
|
|
|
|
sfree(heap);
|
|
sfree(prev);
|
|
sfree(dist);
|
|
sfree(pulls);
|
|
|
|
if (grid[py*w+px] == TARGET)
|
|
grid[py*w+px] = PLAYERTARGET;
|
|
else
|
|
grid[py*w+px] = PLAYER;
|
|
}
|
|
|
|
static char *new_game_desc(const game_params *params, random_state *rs,
|
|
char **aux, bool interactive)
|
|
{
|
|
int w = params->w, h = params->h;
|
|
char *desc;
|
|
int desclen, descpos, descsize, prev, count;
|
|
unsigned char *grid;
|
|
int i, j;
|
|
|
|
/*
|
|
* FIXME: perhaps some more interesting means of choosing how
|
|
* many moves to try?
|
|
*/
|
|
grid = snewn(w*h, unsigned char);
|
|
sokoban_generate(w, h, grid, w*h, false, rs);
|
|
|
|
desclen = descpos = descsize = 0;
|
|
desc = NULL;
|
|
prev = -1;
|
|
count = 0;
|
|
for (i = 0; i < w*h; i++) {
|
|
if (descsize < desclen + 40) {
|
|
descsize = desclen + 100;
|
|
desc = sresize(desc, descsize, char);
|
|
desc[desclen] = '\0';
|
|
}
|
|
switch (grid[i]) {
|
|
case INITIAL:
|
|
j = 'w'; /* FIXME: make some of these 's'? */
|
|
break;
|
|
case SPACE:
|
|
j = 's';
|
|
break;
|
|
case WALL:
|
|
j = 'w';
|
|
break;
|
|
case TARGET:
|
|
j = 't';
|
|
break;
|
|
case BARREL:
|
|
j = 'b';
|
|
break;
|
|
case BARRELTARGET:
|
|
j = 'f';
|
|
break;
|
|
case DEEP_PIT:
|
|
j = 'd';
|
|
break;
|
|
case PLAYER:
|
|
j = 'u';
|
|
break;
|
|
case PLAYERTARGET:
|
|
j = 'v';
|
|
break;
|
|
default:
|
|
j = '?';
|
|
break;
|
|
}
|
|
assert(j != '?');
|
|
if (j != prev) {
|
|
desc[desclen++] = j;
|
|
descpos = desclen;
|
|
prev = j;
|
|
count = 1;
|
|
} else {
|
|
count++;
|
|
desclen = descpos + sprintf(desc+descpos, "%d", count);
|
|
}
|
|
}
|
|
|
|
sfree(grid);
|
|
|
|
return desc;
|
|
}
|
|
|
|
static const char *validate_desc(const game_params *params, const char *desc)
|
|
{
|
|
int w = params->w, h = params->h;
|
|
int area = 0;
|
|
int nplayers = 0;
|
|
|
|
while (*desc) {
|
|
int c = *desc++;
|
|
int n = 1;
|
|
if (*desc && isdigit((unsigned char)*desc)) {
|
|
n = atoi(desc);
|
|
while (*desc && isdigit((unsigned char)*desc)) desc++;
|
|
}
|
|
|
|
area += n;
|
|
|
|
if (c == PLAYER || c == PLAYERTARGET)
|
|
nplayers += n;
|
|
else if (c == INITIAL || c == SPACE || c == WALL || c == TARGET ||
|
|
c == PIT || c == DEEP_PIT || IS_BARREL(c))
|
|
/* ok */;
|
|
else
|
|
return "Invalid character in game description";
|
|
}
|
|
|
|
if (area > w*h)
|
|
return "Too much data in game description";
|
|
if (area < w*h)
|
|
return "Too little data in game description";
|
|
if (nplayers < 1)
|
|
return "No starting player position specified";
|
|
if (nplayers > 1)
|
|
return "More than one starting player position specified";
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static game_state *new_game(midend *me, const game_params *params,
|
|
const char *desc)
|
|
{
|
|
int w = params->w, h = params->h;
|
|
game_state *state = snew(game_state);
|
|
int i;
|
|
|
|
state->p = *params; /* structure copy */
|
|
state->grid = snewn(w*h, unsigned char);
|
|
state->px = state->py = -1;
|
|
state->completed = false;
|
|
|
|
i = 0;
|
|
|
|
while (*desc) {
|
|
int c = *desc++;
|
|
int n = 1;
|
|
if (*desc && isdigit((unsigned char)*desc)) {
|
|
n = atoi(desc);
|
|
while (*desc && isdigit((unsigned char)*desc)) desc++;
|
|
}
|
|
|
|
if (c == PLAYER || c == PLAYERTARGET) {
|
|
state->py = i / w;
|
|
state->px = i % w;
|
|
c = IS_ON_TARGET(c) ? TARGET : SPACE;
|
|
}
|
|
|
|
while (n-- > 0)
|
|
state->grid[i++] = c;
|
|
}
|
|
|
|
assert(i == w*h);
|
|
assert(state->px != -1 && state->py != -1);
|
|
|
|
return state;
|
|
}
|
|
|
|
static game_state *dup_game(const game_state *state)
|
|
{
|
|
int w = state->p.w, h = state->p.h;
|
|
game_state *ret = snew(game_state);
|
|
|
|
ret->p = state->p; /* structure copy */
|
|
ret->grid = snewn(w*h, unsigned char);
|
|
memcpy(ret->grid, state->grid, w*h);
|
|
ret->px = state->px;
|
|
ret->py = state->py;
|
|
ret->completed = state->completed;
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void free_game(game_state *state)
|
|
{
|
|
sfree(state->grid);
|
|
sfree(state);
|
|
}
|
|
|
|
static char *solve_game(const game_state *state, const game_state *currstate,
|
|
const char *aux, const char **error)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
static bool game_can_format_as_text_now(const game_params *params)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
static char *game_text_format(const game_state *state)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
static game_ui *new_ui(const game_state *state)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
static void free_ui(game_ui *ui)
|
|
{
|
|
}
|
|
|
|
static char *encode_ui(const game_ui *ui)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
static void decode_ui(game_ui *ui, const char *encoding)
|
|
{
|
|
}
|
|
|
|
static void game_changed_state(game_ui *ui, const game_state *oldstate,
|
|
const game_state *newstate)
|
|
{
|
|
}
|
|
|
|
struct game_drawstate {
|
|
game_params p;
|
|
int tilesize;
|
|
bool started;
|
|
unsigned short *grid;
|
|
};
|
|
|
|
#define PREFERRED_TILESIZE 32
|
|
#define TILESIZE (ds->tilesize)
|
|
#define BORDER (TILESIZE)
|
|
#define HIGHLIGHT_WIDTH (TILESIZE / 10)
|
|
#define COORD(x) ( (x) * TILESIZE + BORDER )
|
|
#define FROMCOORD(x) ( ((x) - BORDER + TILESIZE) / TILESIZE - 1 )
|
|
|
|
/*
|
|
* I'm going to need to do most of the move-type analysis in both
|
|
* interpret_move and execute_move, so I'll abstract it out into a
|
|
* subfunction. move_type() returns -1 for an illegal move, 0 for a
|
|
* movement, and 1 for a push.
|
|
*/
|
|
int move_type(const game_state *state, int dx, int dy)
|
|
{
|
|
int w = state->p.w, h = state->p.h;
|
|
int px = state->px, py = state->py;
|
|
int nx, ny, nbx, nby;
|
|
|
|
assert(dx >= -1 && dx <= +1);
|
|
assert(dy >= -1 && dy <= +1);
|
|
assert(dx || dy);
|
|
|
|
nx = px + dx;
|
|
ny = py + dy;
|
|
|
|
/*
|
|
* Disallow any move that goes off the grid.
|
|
*/
|
|
if (nx < 0 || nx >= w || ny < 0 || ny >= h)
|
|
return -1;
|
|
|
|
/*
|
|
* Examine the target square of the move to see whether it's a
|
|
* space, a barrel, or a wall.
|
|
*/
|
|
|
|
if (state->grid[ny*w+nx] == WALL ||
|
|
state->grid[ny*w+nx] == PIT ||
|
|
state->grid[ny*w+nx] == DEEP_PIT)
|
|
return -1; /* this one's easy; just disallow it */
|
|
|
|
if (IS_BARREL(state->grid[ny*w+nx])) {
|
|
/*
|
|
* This is a push move. For a start, that means it must not
|
|
* be diagonal.
|
|
*/
|
|
if (dy && dx)
|
|
return -1;
|
|
|
|
/*
|
|
* Now find the location of the third square involved in
|
|
* the push, and stop if it's off the edge.
|
|
*/
|
|
nbx = nx + dx;
|
|
nby = ny + dy;
|
|
if (nbx < 0 || nbx >= w || nby < 0 || nby >= h)
|
|
return -1;
|
|
|
|
/*
|
|
* That third square must be able to accept a barrel.
|
|
*/
|
|
if (state->grid[nby*w+nbx] == SPACE ||
|
|
state->grid[nby*w+nbx] == TARGET ||
|
|
state->grid[nby*w+nbx] == PIT ||
|
|
state->grid[nby*w+nbx] == DEEP_PIT) {
|
|
/*
|
|
* The push is valid.
|
|
*/
|
|
return 1;
|
|
} else {
|
|
return -1;
|
|
}
|
|
} else {
|
|
/*
|
|
* This is just an ordinary move. We've already checked the
|
|
* target square, so the only thing left to check is that a
|
|
* diagonal move has a space on one side to have notionally
|
|
* gone through.
|
|
*/
|
|
if (dx && dy &&
|
|
state->grid[(py+dy)*w+px] != SPACE &&
|
|
state->grid[(py+dy)*w+px] != TARGET &&
|
|
state->grid[py*w+(px+dx)] != SPACE &&
|
|
state->grid[py*w+(px+dx)] != TARGET)
|
|
return -1;
|
|
|
|
/*
|
|
* Otherwise, the move is valid.
|
|
*/
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static char *interpret_move(const game_state *state, game_ui *ui,
|
|
const game_drawstate *ds,
|
|
int x, int y, int button)
|
|
{
|
|
int dx=0, dy=0;
|
|
char *move;
|
|
|
|
/*
|
|
* Diagonal movement is supported as it is in NetHack: it's
|
|
* for movement only (never pushing), and one of the two
|
|
* squares adjacent to both the source and destination
|
|
* squares must be free to move through. In other words, it
|
|
* is only a shorthand for two orthogonal moves and cannot
|
|
* change the nature of the actual puzzle game.
|
|
*/
|
|
if (button == CURSOR_UP || button == (MOD_NUM_KEYPAD | '8'))
|
|
dx = 0, dy = -1;
|
|
else if (button == CURSOR_DOWN || button == (MOD_NUM_KEYPAD | '2'))
|
|
dx = 0, dy = +1;
|
|
else if (button == CURSOR_LEFT || button == (MOD_NUM_KEYPAD | '4'))
|
|
dx = -1, dy = 0;
|
|
else if (button == CURSOR_RIGHT || button == (MOD_NUM_KEYPAD | '6'))
|
|
dx = +1, dy = 0;
|
|
else if (button == (MOD_NUM_KEYPAD | '7'))
|
|
dx = -1, dy = -1;
|
|
else if (button == (MOD_NUM_KEYPAD | '9'))
|
|
dx = +1, dy = -1;
|
|
else if (button == (MOD_NUM_KEYPAD | '1'))
|
|
dx = -1, dy = +1;
|
|
else if (button == (MOD_NUM_KEYPAD | '3'))
|
|
dx = +1, dy = +1;
|
|
else if (button == LEFT_BUTTON)
|
|
{
|
|
if(x < COORD(state->px))
|
|
dx = -1;
|
|
else if (x > COORD(state->px + 1))
|
|
dx = 1;
|
|
if(y < COORD(state->py))
|
|
dy = -1;
|
|
else if (y > COORD(state->py + 1))
|
|
dy = 1;
|
|
}
|
|
else
|
|
return NULL;
|
|
|
|
if((dx == 0) && (dy == 0))
|
|
return(NULL);
|
|
|
|
if (move_type(state, dx, dy) < 0)
|
|
return NULL;
|
|
|
|
move = snewn(2, char);
|
|
move[1] = '\0';
|
|
move[0] = '5' - 3*dy + dx;
|
|
return move;
|
|
}
|
|
|
|
static game_state *execute_move(const game_state *state, const char *move)
|
|
{
|
|
int w = state->p.w, h = state->p.h;
|
|
int px = state->px, py = state->py;
|
|
int dx, dy, nx, ny, nbx, nby, type, m, i;
|
|
bool freebarrels, freetargets;
|
|
game_state *ret;
|
|
|
|
if (*move < '1' || *move == '5' || *move > '9' || move[1])
|
|
return NULL; /* invalid move string */
|
|
|
|
m = *move - '0';
|
|
dx = (m + 2) % 3 - 1;
|
|
dy = 2 - (m + 2) / 3;
|
|
type = move_type(state, dx, dy);
|
|
if (type < 0)
|
|
return NULL;
|
|
|
|
ret = dup_game(state);
|
|
|
|
nx = px + dx;
|
|
ny = py + dy;
|
|
nbx = nx + dx;
|
|
nby = ny + dy;
|
|
|
|
if (type) {
|
|
int b;
|
|
|
|
/*
|
|
* Push.
|
|
*/
|
|
b = ret->grid[ny*w+nx];
|
|
if (IS_ON_TARGET(b)) {
|
|
ret->grid[ny*w+nx] = TARGET;
|
|
b = DETARGETISE(b);
|
|
} else
|
|
ret->grid[ny*w+nx] = SPACE;
|
|
|
|
if (ret->grid[nby*w+nbx] == PIT)
|
|
ret->grid[nby*w+nbx] = SPACE;
|
|
else if (ret->grid[nby*w+nbx] == DEEP_PIT)
|
|
/* do nothing - the pit eats the barrel and remains there */;
|
|
else if (ret->grid[nby*w+nbx] == TARGET)
|
|
ret->grid[nby*w+nbx] = TARGETISE(b);
|
|
else
|
|
ret->grid[nby*w+nbx] = b;
|
|
}
|
|
|
|
ret->px = nx;
|
|
ret->py = ny;
|
|
|
|
/*
|
|
* Check for completion. This is surprisingly complicated,
|
|
* given the presence of pits and deep pits, and also the fact
|
|
* that some Sokoban levels with pits have fewer pits than
|
|
* barrels (due to providing spares, e.g. NetHack's). I think
|
|
* the completion condition in fact must be that the game
|
|
* cannot become any _more_ complete. That is, _either_ there
|
|
* are no remaining barrels not on targets, _or_ there is a
|
|
* good reason why any such barrels cannot be placed. The only
|
|
* available good reason is that there are no remaining pits,
|
|
* no free target squares, and no deep pits at all.
|
|
*/
|
|
if (!ret->completed) {
|
|
freebarrels = false;
|
|
freetargets = false;
|
|
for (i = 0; i < w*h; i++) {
|
|
int v = ret->grid[i];
|
|
|
|
if (IS_BARREL(v) && !IS_ON_TARGET(v))
|
|
freebarrels = true;
|
|
if (v == DEEP_PIT || v == PIT ||
|
|
(!IS_BARREL(v) && IS_ON_TARGET(v)))
|
|
freetargets = true;
|
|
}
|
|
|
|
if (!freebarrels || !freetargets)
|
|
ret->completed = true;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/* ----------------------------------------------------------------------
|
|
* Drawing routines.
|
|
*/
|
|
|
|
static void game_compute_size(const game_params *params, int tilesize,
|
|
int *x, int *y)
|
|
{
|
|
/* Ick: fake up `ds->tilesize' for macro expansion purposes */
|
|
struct { int tilesize; } ads, *ds = &ads;
|
|
ads.tilesize = tilesize;
|
|
|
|
*x = 2 * BORDER + 1 + params->w * TILESIZE;
|
|
*y = 2 * BORDER + 1 + params->h * TILESIZE;
|
|
}
|
|
|
|
static void game_set_size(drawing *dr, game_drawstate *ds,
|
|
const game_params *params, int tilesize)
|
|
{
|
|
ds->tilesize = tilesize;
|
|
}
|
|
|
|
static float *game_colours(frontend *fe, int *ncolours)
|
|
{
|
|
float *ret = snewn(3 * NCOLOURS, float);
|
|
int i;
|
|
|
|
game_mkhighlight(fe, ret, COL_BACKGROUND, COL_HIGHLIGHT, COL_LOWLIGHT);
|
|
|
|
ret[COL_OUTLINE * 3 + 0] = 0.0F;
|
|
ret[COL_OUTLINE * 3 + 1] = 0.0F;
|
|
ret[COL_OUTLINE * 3 + 2] = 0.0F;
|
|
|
|
ret[COL_PLAYER * 3 + 0] = 0.0F;
|
|
ret[COL_PLAYER * 3 + 1] = 1.0F;
|
|
ret[COL_PLAYER * 3 + 2] = 0.0F;
|
|
|
|
ret[COL_BARREL * 3 + 0] = 0.6F;
|
|
ret[COL_BARREL * 3 + 1] = 0.3F;
|
|
ret[COL_BARREL * 3 + 2] = 0.0F;
|
|
|
|
ret[COL_TARGET * 3 + 0] = ret[COL_LOWLIGHT * 3 + 0];
|
|
ret[COL_TARGET * 3 + 1] = ret[COL_LOWLIGHT * 3 + 1];
|
|
ret[COL_TARGET * 3 + 2] = ret[COL_LOWLIGHT * 3 + 2];
|
|
|
|
ret[COL_PIT * 3 + 0] = ret[COL_LOWLIGHT * 3 + 0] / 2;
|
|
ret[COL_PIT * 3 + 1] = ret[COL_LOWLIGHT * 3 + 1] / 2;
|
|
ret[COL_PIT * 3 + 2] = ret[COL_LOWLIGHT * 3 + 2] / 2;
|
|
|
|
ret[COL_DEEP_PIT * 3 + 0] = 0.0F;
|
|
ret[COL_DEEP_PIT * 3 + 1] = 0.0F;
|
|
ret[COL_DEEP_PIT * 3 + 2] = 0.0F;
|
|
|
|
ret[COL_TEXT * 3 + 0] = 1.0F;
|
|
ret[COL_TEXT * 3 + 1] = 1.0F;
|
|
ret[COL_TEXT * 3 + 2] = 1.0F;
|
|
|
|
ret[COL_GRID * 3 + 0] = ret[COL_LOWLIGHT * 3 + 0];
|
|
ret[COL_GRID * 3 + 1] = ret[COL_LOWLIGHT * 3 + 1];
|
|
ret[COL_GRID * 3 + 2] = ret[COL_LOWLIGHT * 3 + 2];
|
|
|
|
ret[COL_OUTLINE * 3 + 0] = 0.0F;
|
|
ret[COL_OUTLINE * 3 + 1] = 0.0F;
|
|
ret[COL_OUTLINE * 3 + 2] = 0.0F;
|
|
|
|
for (i = 0; i < 3; i++) {
|
|
ret[COL_WALL * 3 + i] = (3 * ret[COL_BACKGROUND * 3 + i] +
|
|
1 * ret[COL_HIGHLIGHT * 3 + i]) / 4;
|
|
}
|
|
|
|
*ncolours = NCOLOURS;
|
|
return ret;
|
|
}
|
|
|
|
static game_drawstate *game_new_drawstate(drawing *dr, const game_state *state)
|
|
{
|
|
int w = state->p.w, h = state->p.h;
|
|
struct game_drawstate *ds = snew(struct game_drawstate);
|
|
int i;
|
|
|
|
ds->tilesize = 0;
|
|
ds->p = state->p; /* structure copy */
|
|
ds->grid = snewn(w*h, unsigned short);
|
|
for (i = 0; i < w*h; i++)
|
|
ds->grid[i] = INVALID;
|
|
ds->started = false;
|
|
|
|
return ds;
|
|
}
|
|
|
|
static void game_free_drawstate(drawing *dr, game_drawstate *ds)
|
|
{
|
|
sfree(ds->grid);
|
|
sfree(ds);
|
|
}
|
|
|
|
static void draw_tile(drawing *dr, game_drawstate *ds, int x, int y, int v)
|
|
{
|
|
int tx = COORD(x), ty = COORD(y);
|
|
int bg = (v & 0x100 ? COL_HIGHLIGHT : COL_BACKGROUND);
|
|
|
|
v &= 0xFF;
|
|
|
|
clip(dr, tx+1, ty+1, TILESIZE-1, TILESIZE-1);
|
|
draw_rect(dr, tx+1, ty+1, TILESIZE-1, TILESIZE-1, bg);
|
|
|
|
if (v == WALL) {
|
|
int coords[6];
|
|
|
|
coords[0] = tx + TILESIZE;
|
|
coords[1] = ty + TILESIZE;
|
|
coords[2] = tx + TILESIZE;
|
|
coords[3] = ty + 1;
|
|
coords[4] = tx + 1;
|
|
coords[5] = ty + TILESIZE;
|
|
draw_polygon(dr, coords, 3, COL_LOWLIGHT, COL_LOWLIGHT);
|
|
|
|
coords[0] = tx + 1;
|
|
coords[1] = ty + 1;
|
|
draw_polygon(dr, coords, 3, COL_HIGHLIGHT, COL_HIGHLIGHT);
|
|
|
|
draw_rect(dr, tx + 1 + HIGHLIGHT_WIDTH, ty + 1 + HIGHLIGHT_WIDTH,
|
|
TILESIZE - 2*HIGHLIGHT_WIDTH,
|
|
TILESIZE - 2*HIGHLIGHT_WIDTH, COL_WALL);
|
|
} else if (v == PIT) {
|
|
draw_circle(dr, tx + TILESIZE/2, ty + TILESIZE/2,
|
|
TILESIZE*3/7, COL_PIT, COL_OUTLINE);
|
|
} else if (v == DEEP_PIT) {
|
|
draw_circle(dr, tx + TILESIZE/2, ty + TILESIZE/2,
|
|
TILESIZE*3/7, COL_DEEP_PIT, COL_OUTLINE);
|
|
} else {
|
|
if (IS_ON_TARGET(v)) {
|
|
draw_circle(dr, tx + TILESIZE/2, ty + TILESIZE/2,
|
|
TILESIZE*3/7, COL_TARGET, COL_OUTLINE);
|
|
}
|
|
if (IS_PLAYER(v)) {
|
|
draw_circle(dr, tx + TILESIZE/2, ty + TILESIZE/2,
|
|
TILESIZE/3, COL_PLAYER, COL_OUTLINE);
|
|
} else if (IS_BARREL(v)) {
|
|
char str[2];
|
|
|
|
draw_circle(dr, tx + TILESIZE/2, ty + TILESIZE/2,
|
|
TILESIZE/3, COL_BARREL, COL_OUTLINE);
|
|
str[1] = '\0';
|
|
str[0] = BARREL_LABEL(v);
|
|
if (str[0]) {
|
|
draw_text(dr, tx + TILESIZE/2, ty + TILESIZE/2,
|
|
FONT_VARIABLE, TILESIZE/2,
|
|
ALIGN_VCENTRE | ALIGN_HCENTRE, COL_TEXT, str);
|
|
}
|
|
}
|
|
}
|
|
|
|
unclip(dr);
|
|
draw_update(dr, tx, ty, TILESIZE, TILESIZE);
|
|
}
|
|
|
|
static void game_redraw(drawing *dr, game_drawstate *ds,
|
|
const game_state *oldstate, const game_state *state,
|
|
int dir, const game_ui *ui,
|
|
float animtime, float flashtime)
|
|
{
|
|
int w = state->p.w, h = state->p.h /*, wh = w*h */;
|
|
int x, y;
|
|
int flashtype;
|
|
|
|
if (flashtime &&
|
|
!((int)(flashtime * 3 / FLASH_LENGTH) % 2))
|
|
flashtype = 0x100;
|
|
else
|
|
flashtype = 0;
|
|
|
|
/*
|
|
* Initialise a fresh drawstate.
|
|
*/
|
|
if (!ds->started) {
|
|
/*
|
|
* Draw the grid lines.
|
|
*/
|
|
for (y = 0; y <= h; y++)
|
|
draw_line(dr, COORD(0), COORD(y), COORD(w), COORD(y),
|
|
COL_LOWLIGHT);
|
|
for (x = 0; x <= w; x++)
|
|
draw_line(dr, COORD(x), COORD(0), COORD(x), COORD(h),
|
|
COL_LOWLIGHT);
|
|
|
|
ds->started = true;
|
|
}
|
|
|
|
/*
|
|
* Draw the grid contents.
|
|
*/
|
|
for (y = 0; y < h; y++)
|
|
for (x = 0; x < w; x++) {
|
|
int v = state->grid[y*w+x];
|
|
if (y == state->py && x == state->px) {
|
|
if (v == TARGET)
|
|
v = PLAYERTARGET;
|
|
else {
|
|
assert(v == SPACE);
|
|
v = PLAYER;
|
|
}
|
|
}
|
|
|
|
v |= flashtype;
|
|
|
|
if (ds->grid[y*w+x] != v) {
|
|
draw_tile(dr, ds, x, y, v);
|
|
ds->grid[y*w+x] = v;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
static float game_anim_length(const game_state *oldstate,
|
|
const game_state *newstate, int dir, game_ui *ui)
|
|
{
|
|
return 0.0F;
|
|
}
|
|
|
|
static float game_flash_length(const game_state *oldstate,
|
|
const game_state *newstate, int dir, game_ui *ui)
|
|
{
|
|
if (!oldstate->completed && newstate->completed)
|
|
return FLASH_LENGTH;
|
|
else
|
|
return 0.0F;
|
|
}
|
|
|
|
static void game_get_cursor_location(const game_ui *ui,
|
|
const game_drawstate *ds,
|
|
const game_state *state,
|
|
const game_params *params,
|
|
int *x, int *y, int *w, int *h)
|
|
{
|
|
}
|
|
|
|
static int game_status(const game_state *state)
|
|
{
|
|
return state->completed ? +1 : 0;
|
|
}
|
|
|
|
static bool game_timing_state(const game_state *state, game_ui *ui)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
static void game_print_size(const game_params *params, float *x, float *y)
|
|
{
|
|
}
|
|
|
|
static void game_print(drawing *dr, const game_state *state, int tilesize)
|
|
{
|
|
}
|
|
|
|
#ifdef COMBINED
|
|
#define thegame sokoban
|
|
#endif
|
|
|
|
const struct game thegame = {
|
|
"Sokoban", NULL, NULL,
|
|
default_params,
|
|
game_fetch_preset, NULL,
|
|
decode_params,
|
|
encode_params,
|
|
free_params,
|
|
dup_params,
|
|
true, game_configure, custom_params,
|
|
validate_params,
|
|
new_game_desc,
|
|
validate_desc,
|
|
new_game,
|
|
dup_game,
|
|
free_game,
|
|
false, solve_game,
|
|
false, game_can_format_as_text_now, game_text_format,
|
|
new_ui,
|
|
free_ui,
|
|
encode_ui,
|
|
decode_ui,
|
|
NULL, /* game_request_keys */
|
|
game_changed_state,
|
|
interpret_move,
|
|
execute_move,
|
|
PREFERRED_TILESIZE, game_compute_size, game_set_size,
|
|
game_colours,
|
|
game_new_drawstate,
|
|
game_free_drawstate,
|
|
game_redraw,
|
|
game_anim_length,
|
|
game_flash_length,
|
|
game_get_cursor_location,
|
|
game_status,
|
|
false, false, game_print_size, game_print,
|
|
false, /* wants_statusbar */
|
|
false, game_timing_state,
|
|
0, /* flags */
|
|
};
|