1 /*
2  * enigma/engine.c - the main game engine of Enigma. Provides the
3  * make_move() function, which is given a move and a structure
4  * containing a game state, and transforms the game state to
5  * reflect the consequences of the move.
6  *
7  * Copyright 2000 Simon Tatham. All rights reserved.
8  *
9  * Enigma is licensed under the MIT licence. See the file LICENCE for
10  * details.
11  *
12  * - we are all amf -
13  */
14 
15 #include <stdio.h>
16 #include <stdlib.h>
17 #include <string.h>
18 #include <ctype.h>
19 #include <assert.h>
20 
21 #include "enigma.h"
22 
23 #define addlist(pos,list) do { \
24 	pos = smalloc(sizeof(posn)); \
25 	list##tail.prev->next = pos; \
26 	pos->prev = list##tail.prev; \
27 	pos->next = &list##tail; \
28 	list##tail.prev = pos; \
29     } while (0)
30 
31 #define remlist(pos) do { \
32 	pos->next->prev = pos->prev; \
33 	pos->prev->next = pos->next; \
34 	free (pos); \
35     } while (0)
36 
37 #define C_w ('w' & 0x1F)  /* Ctrl-W */
38 #define C_x ('x' & 0x1F)  /* Ctrl-X */
39 #define C_y ('y' & 0x1F)  /* Ctrl-Y */
40 #define C_z ('z' & 0x1F)  /* Ctrl-Z */
41 
42 /*
43  * Set up a game_state from a level.
44  */
init_game(level * lev)45 gamestate *init_game (level *lev) {
46     gamestate *ret;
47     int i, j;
48 
49     ret = gamestate_new(lev->width, lev->height, lev->flags);
50     memcpy(ret->leveldata, lev->leveldata, lev->width * lev->height);
51     ret->title = lev->title;
52     ret->movenum = 0;
53     ret->gold_got = 0;
54     ret->status = PLAYING;
55     ret->gold_total = 0;
56     for (j = 0; j < ret->height; j++) {
57 	for (i = 0; i < ret->width; i++) {
58 	    if (ret->leveldata[j*ret->width+i] == '$')
59 		ret->gold_total++;
60 	    if (ret->leveldata[j*ret->width+i] == '@') {
61 		ret->player_x = i;
62 		ret->player_y = j;
63 	    }
64 	}
65     }
66     return ret;
67 }
68 
69 #define destroyable(x,y) \
70     ((x) > 0 && (x) < w-1 && (y) > 0 && (y) < h-1 && \
71      ret->leveldata[w*((y)-1)+(x)] != '~' && \
72      ret->leveldata[w*((y)+1)+(x)] != '~' && \
73      ret->leveldata[w*(y)+(x)-1] != '~' && \
74      ret->leveldata[w*(y)+(x)+1] != '~')
75 
76 #define pri(p) (ret->flags & LEVEL_REL_PRIORITY ? (p) : 1)
77 
78 /*
79  * 'key' is h, j, k, l for movement, or x to switch player pieces
80  */
make_move(gamestate * state,char key)81 gamestate *make_move (gamestate *state, char key) {
82     gamestate *ret;
83     int xfrom, yfrom, pfrom, xto, yto, pto, pdx, pdy, pdp;
84     int i, j, k;
85     int w, h;
86     typedef struct posn posn;
87     struct posn {
88 	posn *next, *prev;
89 	int x, y;
90 	int vertical;
91     };
92     posn movehead = { NULL, NULL, -1, -1, 0 };
93     posn movetail = { NULL, NULL, -1, -1, 0 };
94     posn nmovhead = { NULL, NULL, -1, -1, 0 };
95     posn nmovtail = { NULL, NULL, -1, -1, 0 };
96     posn expshead = { NULL, NULL, -1, -1, 0 };
97     posn expstail = { NULL, NULL, -1, -1, 0 };
98     posn *pos;
99 
100     /*
101      * initialise those lists
102      */
103     movehead.next = &movetail; movetail.prev = &movehead;
104     nmovhead.next = &nmovtail; nmovtail.prev = &nmovhead;
105     expshead.next = &expstail; expstail.prev = &expshead;
106 
107     /*
108      * copy most of the state
109      */
110     ret = gamestate_copy(state);
111 
112     /*
113      * get the level dimensions
114      */
115     w = ret->width;
116     h = ret->height;
117 
118     /*
119      * sanity check the state
120      */
121     if (ret->leveldata[w*ret->player_y+ret->player_x] != '@') {
122 	ret->status = BROKEN;
123 	return ret;
124     }
125 
126     /*
127      * Handle the special `x' move. When we add this to the stored
128      * sequence, we make the marginal optimisation of removing a
129      * prior x if present rather than adding a new one, so that the
130      * stored move sequence will only ever contain a single x at a
131      * time.
132      */
133     if (key == 'x') {
134 	for (i = 0; i < w; i++)
135 	    for (j = 0; j < h; j++)
136 		if (ret->leveldata[w*j+i] == 'O') {
137 		    ret->leveldata[w*j+i] = '@';
138 		    ret->leveldata[w*ret->player_y+ret->player_x] = 'O';
139 		    ret->player_y = j;
140 		    ret->player_x = i;
141 
142 		    if (ret->movenum > 0 &&
143 			ret->sequence[ret->movenum-1] == 'x') {
144 			ret->movenum--;
145 		    } else {
146 			ret->movenum++;
147 
148 			if (ret->movenum > ret->sequence_size) {
149 			    ret->sequence_size = ret->movenum + 128;
150 			    ret->sequence = srealloc(ret->sequence,
151 						     ret->sequence_size);
152 			}
153 			ret->sequence[ret->movenum-1] = key;
154 		    }
155 
156 		    return ret;
157 		}
158 	/*
159 	 * Otherwise, we cannot swap at all, so do nothing.
160 	 */
161 	return ret;
162     }
163 
164     /*
165      * determine move destination
166      */
167     xto = xfrom = ret->player_x;
168     yto = yfrom = ret->player_y;
169     switch (key) {
170       case 'h': xto--; break; case 'l': xto++; break;
171       case 'k': yto--; break; case 'j': yto++; break;
172     }
173     pfrom = yfrom * w + xfrom;
174     pto = yto * w + xto;
175     pdx = xto - xfrom;
176     pdy = yto - yfrom;
177     pdp = pto - pfrom;
178     i = ret->leveldata[pto];
179 
180     /*
181      * process teleporters
182      */
183     if (i == '#') {
184 	int x, y = 0;
185 
186 	/*
187 	 * First find the other teleporter.
188 	 */
189 	for (x = 0; x < w; x++) {
190 	    for (y = 0; y < h; y++)
191 		if (w*y+x != pto && ret->leveldata[w*y+x] == '#')
192 		    break;
193 	    if (y < h)
194 		break;
195 	}
196 	if (x == w) {
197 	    /*
198 	     * Couldn't find the other teleporter at all; it must
199 	     * have been destroyed or something. Disallow move.
200 	     */
201 	    return ret;
202 	}
203 
204 	/*
205 	 * Find a suitable emergence point around the other
206 	 * teleporter. We look right, up, left then down, in that
207 	 * order.
208 	 */
209 	if (ret->leveldata[w*y+(x+1)] == ' ') {
210 	    x++;
211 	} else if (ret->leveldata[w*(y-1)+x] == ' ') {
212 	    y--;
213 	} else if (ret->leveldata[w*y+(x-1)] == ' ') {
214 	    x--;
215 	} else if (ret->leveldata[w*(y+1)+x] == ' ') {
216 	    y++;
217 	} else
218 	    return ret;		       /* no free square; disallow move */
219 
220 	/*
221 	 * Continue with the move as usual (we must still do an
222 	 * expose event on the vacated square, etc). Note that from
223 	 * here on, xto and xfrom might be wildly different, but
224 	 * pdx still reflects the logical direction of the move.
225 	 * Ditto yto/yfrom/pdy, pto/pfrom/pdp.
226 	 */
227 	xto = x;
228 	yto = y;
229 	pto = yto * w + xto;
230 	i = ret->leveldata[pto];
231     }
232 
233     /*
234      * disallow the move if the destination isn't movable to
235      */
236     if (i == '+' || i == '|' || i == '-' || i == '%' || i == 'O' ||
237 	(i == 'E' && ret->gold_got < ret->gold_total) ||
238 	(i == '!' && xfrom != xto) ||
239 	(i == '=' && yfrom != yto))
240 	return ret;		       /* do nothing */
241 
242     /*
243      * increment the gold counter if we're moving on to a $
244      */
245     if (i == '$')
246 	ret->gold_got++;
247 
248     /*
249      * is this a push move?
250      */
251     if (i == '8' || i == 'o' ||
252 	i == 'v' || i == '>' || i == '<' || i == '^' ||
253 	i == 'W' || i == 'X' || i == 'Y' || i == 'Z') {
254 	/*
255 	 * Check the next space along to see if we can push into
256 	 * it. Disallow the move otherwise.
257 	 */
258 	j = pto + pdp;
259 	k = ret->leveldata[j];
260 	if (k != ' ' && k != '.' &&
261 	    (k != '!' || !pdy || i == '8' || i == 'o') &&
262 	    (k != '=' || !pdx || i == '8' || i == 'o'))
263 	    return ret;		       /* do nothing */
264 
265 	/*
266 	 * Also disallow the move if we're trying to push something
267 	 * against its direction.
268 	 */
269 	if (((i == 'v' || i == 'X') && pdy == -1) ||
270 	    ((i == '>' || i == 'W') && pdx == -1) ||
271 	    ((i == '<' || i == 'Y') && pdx == +1) ||
272 	    ((i == '^' || i == 'Z') && pdy == +1))
273 	    return ret;		       /* do nothing */
274 
275 	/*
276 	 * Now we know we can make the move. Move the pushed piece,
277 	 * and add it to the moving list if there's a blank in its
278 	 * direction. The movement of the player itself will be
279 	 * performed outside this conditional block.
280 	 */
281 	ret->leveldata[j] = ret->leveldata[pto];
282 	if (((i=='v' || i=='X') &&
283 	     (ret->leveldata[j+w] == ' ' || ret->leveldata[j+w] == '!')) ||
284 	    ((i=='>' || i=='W') &&
285 	     (ret->leveldata[j+1] == ' ' || ret->leveldata[j+1] == '=')) ||
286 	    ((i=='<' || i=='Y') &&
287 	     (ret->leveldata[j-1] == ' ' || ret->leveldata[j-1] == '=')) ||
288 	    ((i=='^' || i=='Z') &&
289 	     (ret->leveldata[j-w] == ' ' || ret->leveldata[j-w] == '!')) ||
290 	    i == 'o') {
291 	    addlist(pos, move);
292 	    pos->x = xto + pdx;
293 	    pos->y = yto + pdy;
294 	    if (ret->leveldata[j] == 'o') {
295 		if (yto-yfrom == -1)
296 		    ret->leveldata[j] = 'a' | 0x80;   /* a is upmoving o */
297 		else if (yto-yfrom == +1)
298 		    ret->leveldata[j] = 'b' | 0x80;   /* b is downmoving o */
299 		else if (xto-xfrom == +1)
300 		    ret->leveldata[j] = 'c' | 0x80;   /* c is rightmoving o */
301 		else if (xto-xfrom == -1)
302 		    ret->leveldata[j] = 'd' | 0x80;   /* d is leftmoving o */
303 	    } else
304 		ret->leveldata[j] |= 0x80;  /* just flag as moving */
305 	}
306     }
307 
308     /*
309      * Move the player and expose the square it has left. Increment
310      * the move count here as well, and add the move to the stored
311      * sequence.
312      */
313     ret->leveldata[pfrom] = ' ';
314     ret->leveldata[pto] = '@';
315     ret->player_x = xto;
316     ret->player_y = yto;
317     ret->movenum++;
318     if (ret->movenum > ret->sequence_size) {
319 	ret->sequence_size = ret->movenum + 128;
320 	ret->sequence = srealloc(ret->sequence, ret->sequence_size);
321     }
322     ret->sequence[ret->movenum-1] = key;
323     addlist(pos, exps);
324     pos->x = xfrom;
325     pos->y = yfrom;
326     pos->vertical = pri(pdy == 0);
327 
328     /*
329      * Kill the player if he walked into a &, or flag the level as
330      * complete if he legitimately walked into the E. In this
331      * situation we still do the movements around him, partly for
332      * looks and mostly because if you walk into the E and a bomb
333      * blows it up in the same move, you _are_ dead.
334      */
335     if (i == '&')
336 	ret->status = DIED;
337     else if (i == 'E' && ret->gold_got >= ret->gold_total)
338 	ret->status = COMPLETED;
339 
340     /*
341      * Now repeatedly process exposed cells and moving objects
342      * until none of either remain.
343      */
344     while (expshead.next->x != -1 || movehead.next->x != -1) {
345 	/*
346 	 * Process exposed cells, which may generate more moving
347 	 * objects.
348 	 */
349 	while (expshead.next->x != -1) {
350 	    posn *p, *q;
351 	    p = expshead.next;
352 	    while (p->x != -1) {
353 		enum { LEFT, RIGHT, UP, DOWN } directions[4];
354 		int dir;
355 
356 		q = p->next;
357 		j = p->y * w + p->x;
358 
359 		if (p->vertical) {
360 		    directions[0] = UP;
361 		    directions[1] = LEFT;
362 		    directions[2] = RIGHT;
363 		    directions[3] = DOWN;
364 		} else {
365 		    directions[0] = LEFT;
366 		    directions[1] = RIGHT;
367 		    directions[2] = UP;
368 		    directions[3] = DOWN;
369 		}
370 
371 		for (dir = 0; dir < 4; dir++) {
372 		    switch (directions[dir]) {
373 		      case UP:
374 			if ((ret->leveldata[j-w] & 0x7F) == 'v' ||
375 			    (ret->leveldata[j-w] & 0x7F) == 'X') {
376 			    /*
377 			     * Falling object above the exposed cell.
378 			     */
379 			    if (!(ret->leveldata[j-w] & 0x80)) {
380 				addlist(pos, move);
381 				pos->x = p->x;
382 				pos->y = p->y-1;
383 				ret->leveldata[j-w] |= 0x80;/* flag as moving */
384 			    }
385 			    dir = 4;   /* terminate loop */
386 			}
387 			break;
388 		      case LEFT:
389 			if ((ret->leveldata[j-1] & 0x7F) == '>' ||
390 			    (ret->leveldata[j-1] & 0x7F) == 'W') {
391 			    /*
392 			     * Right-moving object to the left of the
393 			     * exposed cell.
394 			     */
395 			    if (!(ret->leveldata[j-1] & 0x80)) {
396 				addlist(pos, move);
397 				pos->x = p->x-1;
398 				pos->y = p->y;
399 				ret->leveldata[j-1] |= 0x80;/* flag as moving */
400 			    }
401 			    dir = 4;   /* terminate loop */
402 			}
403 			break;
404 		      case RIGHT:
405 			if ((ret->leveldata[j+1] & 0x7F) == '<' ||
406 			    (ret->leveldata[j+1] & 0x7F) == 'Y') {
407 			    /*
408 			     * Left-moving object to the right of the
409 			     * exposed cell.
410 			     */
411 			    if (!(ret->leveldata[j+1] & 0x80)) {
412 				addlist(pos, move);
413 				pos->x = p->x+1;
414 				pos->y = p->y;
415 				ret->leveldata[j+1] |= 0x80;/* flag as moving */
416 			    }
417 			    dir = 4;   /* terminate loop */
418 			}
419 			break;
420 		      case DOWN:
421 			if ((ret->leveldata[j+w] & 0x7F) == '^' ||
422 			    (ret->leveldata[j+w] & 0x7F) == 'Z') {
423 			    /*
424 			     * Up-moving object below the exposed cell.
425 			     */
426 			    if (!(ret->leveldata[j+w] & 0x80)) {
427 				addlist(pos, move);
428 				pos->x = p->x;
429 				pos->y = p->y+1;
430 				ret->leveldata[j+w] |= 0x80;/* flag as moving */
431 			    }
432 			    dir = 4;   /* terminate loop */
433 			}
434 			break;
435 		    }
436 		}
437 		remlist(p);
438 		p = q;
439 	    }
440 	}
441 
442 	/*
443 	 * Process moving objects, which may generate more exposed
444 	 * cells. We empty the `move' list and fill up the `nmov'
445 	 * list with things which are still moving at the end of
446 	 * the turn. Then we manhandle the new list back into
447 	 * `move'.
448 	 */
449 	nmovhead.next = &nmovtail;
450 	nmovtail.prev = &nmovhead;
451 
452 	while (movehead.next->x != -1) {
453 	    posn *p, *q;
454 	    int dx, dy, dp, bomb, sack;
455 
456 	    p = movehead.next;
457 	    while (p->x != -1) {
458 		q = p->next;
459 		j = p->y * w + p->x;
460 		k = ret->leveldata[j] & 0x7F;
461 		dx = dy = bomb = sack = 0;
462 		switch (k) {
463 		  case 'a': dx =  0; dy = -1; bomb = 0; sack = 1; break;
464 		  case 'b': dx =  0; dy = +1; bomb = 0; sack = 1; break;
465 		  case 'c': dx = +1; dy =  0; bomb = 0; sack = 1; break;
466 		  case 'd': dx = -1; dy =  0; bomb = 0; sack = 1; break;
467 		  case 'v': dx =  0; dy = +1; bomb = 0; sack = 0; break;
468 		  case '>': dx = +1; dy =  0; bomb = 0; sack = 0; break;
469 		  case '<': dx = -1; dy =  0; bomb = 0; sack = 0; break;
470 		  case '^': dx =  0; dy = -1; bomb = 0; sack = 0; break;
471 		  case 'X': dx =  0; dy = +1; bomb = 1; sack = 0; break;
472 		  case 'W': dx = +1; dy =  0; bomb = 1; sack = 0; break;
473 		  case 'Z': dx =  0; dy = -1; bomb = 1; sack = 0; break;
474 		  case 'Y': dx = -1; dy =  0; bomb = 1; sack = 0; break;
475 		  case C_x: dx = +1; dy =  0; bomb = 2; sack = 0; break;
476 		  case C_w: dx =  0; dy = +1; bomb = 2; sack = 0; break;
477 		  case C_z: dx = +1; dy =  0; bomb = 2; sack = 0; break;
478 		  case C_y: dx =  0; dy = +1; bomb = 2; sack = 0; break;
479 		}
480 		dp = dy*w+dx;
481 		if (bomb == 2) {       /* special case: exploding thing */
482 		    ret->leveldata[j] = ' ';   /* remove the bomb */
483 		    addlist(pos, exps);   /* and expose the vacated cell */
484 		    pos->x = p->x;
485 		    pos->y = p->y;
486 		    pos->vertical = pri(dy == 0);
487 		    if (destroyable(p->x-dx, p->y-dy)) {
488 			ret->leveldata[j-dp] = ' ';  /* and the left/up cell */
489 			addlist(pos, exps);  /* expose the vacated cell */
490 			pos->x = p->x - dx;
491 			pos->y = p->y - dy;
492 			pos->vertical = pri(dy == 0);
493 		    }
494 		    if (destroyable(p->x+dx, p->y+dy)) {
495 			ret->leveldata[j+dp] = ' ';  /* the right/down cell */
496 			addlist(pos, exps);  /* expose the vacated cell */
497 			pos->x = p->x + dx;
498 			pos->y = p->y + dy;
499 			pos->vertical = pri(dy == 0);
500 		    }
501 		} else {
502 		    if ((!bomb || (ret->flags & LEVEL_FLIMSY_BOMBS)) &&
503 			!sack && (ret->leveldata[j+dp] == 'X' ||
504 				  ret->leveldata[j+dp] == 'W' ||
505 				  ret->leveldata[j+dp] == 'Y' ||
506 				  ret->leveldata[j+dp] == 'Z')) {
507 			ret->leveldata[j] = ' ';   /* remove the detonator */
508 			addlist(pos, exps);   /* expose the vacated cell */
509 			pos->x = p->x;
510 			pos->y = p->y;
511 			pos->vertical = pri(dy != 0);
512 			ret->leveldata[j+dp] &= 0x1F;   /* move to Ctrl-x */
513 			addlist(pos, nmov);   /* add exploding bomb to list */
514 			pos->x = p->x + dx;
515 			pos->y = p->y + dy;
516 		    } else {
517 			if (ret->leveldata[j+dp] == ' ' ||
518 			    (ret->leveldata[j+dp] == '.' && !sack) ||
519 			    (ret->leveldata[j+dp] == '!' && dy && !sack) ||
520 			    (ret->leveldata[j+dp] == '=' && dx && !sack) ||
521 			    (ret->leveldata[j+dp] == '@' && !sack) ||
522 			    (ret->leveldata[j+dp] == 'O' && !sack)) {
523 			    ret->leveldata[j+dp] = k | 0x80;   /* stay moving */
524 			    ret->leveldata[j] = ' ';   /* fall */
525 			    addlist(pos, exps);  /* expose the vacated cell */
526 			    pos->x = p->x;
527 			    pos->y = p->y;
528 			    pos->vertical = pri(dy != 0);
529 			    addlist(pos, nmov);   /* keep the object moving */
530 			    pos->x = p->x + dx;
531 			    pos->y = p->y + dy;
532 			} else {
533 			    if (sack)
534 				ret->leveldata[j] = 'o';
535 			    else
536 				ret->leveldata[j] &= 0x7F; /* non-moving */
537 			}
538 		    }
539 		}
540 		remlist(p);
541 		p = q;
542 	    }
543 	}
544 
545 	if (nmovhead.next->next) {
546 	    movehead.next = nmovhead.next;
547 	    movetail.prev = nmovtail.prev;
548 	    movehead.next->prev = &movehead;
549 	    movetail.prev->next = &movetail;
550 	} else {		       /* special case - nmov list empty */
551 	    movehead.next = &movetail;
552 	    movetail.prev = &movehead;
553 	}
554     }
555 
556     /*
557      * If we've killed the player at any point, flag him as dead,
558      * unless there is still an `O' on the screen in which case we
559      * transfer to it.
560      */
561     if (ret->leveldata[w*ret->player_y+ret->player_x] != '@') {
562 	for (i = 0; i < w; i++)
563 	    for (j = 0; j < h; j++)
564 		if (ret->leveldata[w*j+i] == 'O') {
565 		    ret->leveldata[w*j+i] = '@';
566 		    ret->player_y = j;
567 		    ret->player_x = i;
568 		    return ret;
569 		}
570 	/*
571 	 * If we reach here, there was no O, so we really are dead.
572 	 */
573 	ret->status = DIED;
574     }
575 
576     /*
577      * I think we're done!
578      */
579     return ret;
580 }
581 
582 /*
583  * Validate a level structure to ensure it contains no obvious
584  * mistakes. Returns an error message in `buffer', or NULL.
585  */
586 #define iswalloroutdoors(c) \
587     ((c) == '+' || (c) == '-' || (c) == '|' || \
588 	 (c) == '%' || (c) == '&' || (c) == '~')
589 
validate(level * level,char * buffer,int buflen)590 char *validate(level *level, char *buffer, int buflen)
591 {
592     int x, y, x1, y1, xO = -1, yO = -1, xP = -1, yP = -1, nt = 0;
593     int c, c1, badsemiwall;
594     int w = level->width, h = level->height;
595     char internalbuf[256];	       /* big enough for any of our sprintfs */
596 
597     for (y = 0; y < h; y++)
598 	for (x = 0; x < w; x++) {
599 
600 	    c = level->leveldata[w*y+x];
601 
602 	    /*
603 	     * Check that the boundary of the level is either solid
604 	     * wall or the `~' outdoors character.
605 	     */
606 	    if ((x == 0 || y == 0 || x == w-1 || y == h-1) &&
607 		!iswalloroutdoors(c)) {
608 		sprintf(internalbuf, "level boundary is not wall or `~' at"
609 			" line %d column %d", y, x);
610 		strncpy(buffer, internalbuf, buflen);
611 		buffer[buflen-1] = '\0';
612 		return buffer;
613 	    }
614 
615 	    /*
616 	     * Check that any `~' outdoors character is adjacent
617 	     * only to walls and other `~'.
618 	     */
619 	    if (c == '~' &&
620 		((x > 0 && !iswalloroutdoors(level->leveldata[w*y+(x-1)])) ||
621 		 (x < w-1 && !iswalloroutdoors(level->leveldata[w*y+(x+1)])) ||
622 		 (y > 0 && !iswalloroutdoors(level->leveldata[w*(y-1)+x])) ||
623 		 (y < h-1 && !iswalloroutdoors(level->leveldata[w*(y+1)+x]))))
624 	    {
625 		sprintf(internalbuf, "`~' is adjacent to something other than"
626 			" wall or `~' at line %d column %d", y, x);
627 		strncpy(buffer, internalbuf, buflen);
628 		buffer[buflen-1] = '\0';
629 		return buffer;
630 	    }
631 
632 	    /*
633 	     * Ensure there is at most one secondary player
634 	     * starting point.
635 	     */
636 	    if (c == 'O') {
637 		if (xO == -1 && yO == -1)
638 		    xO = x, yO = y;
639 		else {
640 		    sprintf(internalbuf, "multiple `O's at line %d column %d"
641 			    " and line %d column %d", yO, xO, y, x);
642 		    strncpy(buffer, internalbuf, buflen);
643 		    buffer[buflen-1] = '\0';
644 		    return buffer;
645 		}
646 	    }
647 
648 	    /*
649 	     * Ensure there is precisely one primary player
650 	     * starting point.
651 	     */
652 	    if (c == '@') {
653 		if (xP == -1 && yP == -1)
654 		    xP = x, yP = y;
655 		else {
656 		    sprintf(internalbuf, "multiple `@'s at line %d column %d"
657 			    " and line %d column %d", yP, xP, y, x);
658 		    strncpy(buffer, internalbuf, buflen);
659 		    buffer[buflen-1] = '\0';
660 		    return buffer;
661 		}
662 	    }
663 
664 	    /*
665 	     * Count teleporters.
666 	     */
667 	    if (c == '#') nt++;
668 
669 	    switch (c) {
670 	      case 'v': case 'X':
671 		/* Down-falling piece; check below it. */
672 		x1 = x; y1 = y + 1; badsemiwall = '!'; break;
673 	      case '>': case 'W':
674 		/* Right-falling piece; check to its right. */
675 		x1 = x + 1; y1 = y; badsemiwall = '='; break;
676 	      case '^': case 'Z':
677 		/* Up-falling piece; check above it. */
678 		x1 = x; y1 = y - 1; badsemiwall = '!'; break;
679 	      case '<': case 'Y':
680 		/* Left-falling piece; check to its left. */
681 		x1 = x - 1; y1 = y; badsemiwall = '='; break;
682 	      case '@': case '$': case '%': case '-': case '+': case '|':
683 	      case '&': case 'E': case 'o': case '8': case '.': case ':':
684 	      case ' ': case '~': case '!': case '=': case 'O': case '#':
685 		/* Non-falling piece; carry on. */
686 		continue;
687 	      default:
688 		/* Unrecognised character. */
689 		if (isprint(c))
690 		    sprintf(internalbuf, "unrecognised character `%c' at"
691 			    " line %d column %d", c, y, x);
692 		else
693 		    sprintf(internalbuf, "unrecognised character %d at"
694 			    " line %d column %d", (unsigned char)c, y, x);
695 		strncpy(buffer, internalbuf, buflen);
696 		buffer[buflen-1] = '\0';
697 		return buffer;
698 	    }
699 
700 	    /*
701 	     * This is an assert rather than a validation check,
702 	     * because the boundary check above should have ruled
703 	     * out the possibility of it failing _now_ by failing
704 	     * earlier.
705 	     */
706 	    assert(x1 >= 0 && x1 < w && y1 >= 0 && y1 < h);
707 	    c1 = level->leveldata[w*y1+x1];
708 	    if (c1 == ' ') {
709 		sprintf(internalbuf, "`%c' piece resting on thin air"
710 			" at line %d column %d", c, y, x);
711 		strncpy(buffer, internalbuf, buflen);
712 		buffer[buflen-1] = '\0';
713 		return buffer;
714 	    }
715 	    if (c1 == badsemiwall) {
716 		sprintf(internalbuf, "`%c' piece trying to rest on `%c'"
717 			" at line %d column %d", c, badsemiwall, y, x);
718 		strncpy(buffer, internalbuf, buflen);
719 		buffer[buflen-1] = '\0';
720 		return buffer;
721 	    }
722 	}
723 
724     if (xP == -1 && yP == -1) {
725 	sprintf(internalbuf, "no `@' in level");
726 	strncpy(buffer, internalbuf, buflen);
727 	buffer[buflen-1] = '\0';
728 	return buffer;
729     }
730 
731     /*
732      * There should be exactly 0 or 2 teleporters.
733      */
734     if (nt != 0 && nt != 2) {
735 	sprintf(internalbuf, "%d teleporters in level (should be 0 or 2)", nt);
736 	strncpy(buffer, internalbuf, buflen);
737 	buffer[buflen-1] = '\0';
738 	return buffer;
739     }
740 
741     return NULL;
742 }
743