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