1 /*
2  * Copyright (c) 2019 Georg Brein. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are met:
6  *
7  * 1. Redistributions of source code must retain the above copyright notice,
8  *    this list of conditions and the following disclaimer.
9  *
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * 3. Neither the name of the copyright holder nor the names of its
15  *    contributors may be used to endorse or promote products derived from
16  *    this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
22  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28  * POSSIBILITY OF SUCH DAMAGE.
29  */
30 
31 
32 /*
33  * This is a simple text-based implementation of a minesweeper-like
34  * game as a demonstration of tnylpo; it expects tnylpo's VT52
35  * emulation and the tnylpo character set.
36  *
37  * This program was developed using the HI-TECH Z80 CP/M C Compiler V3.09;
38  * Compile it with the command "tnylpo -b c -o mine.c", which should produce
39  * the executable file "mine.com".
40  *
41  * Execute it with the command "tnylpo -f mine.conf mine".
42  */
43 
44 /*
45  * BIOS functions
46  */
47 extern int bios(int, ...);
48 
49 #define WBOOT 1
50 #define CONST 2
51 #define CONIN 3
52 #define CONOUT 4
53 
54 
55 /*
56  * display size
57  */
58 #define X_SIZE 80
59 #define Y_SIZE 24
60 
61 
62 /*
63  * key codes
64  */
65 #define KEY_CTRLD 0x04
66 #define KEY_CTRLE 0x05
67 #define KEY_TAB 0x09
68 #define KEY_CR 0x0d
69 #define KEY_CTRLS 0x13
70 #define KEY_CTRLX 0x18
71 #define KEY_ESC 0x1b
72 
73 
74 /*
75  * representation of a mine, a flag, and a falsely flagged square on
76  * the screen
77  */
78 #define MINE '\xa4'
79 #define FLAG 'F'
80 #define WRONG '*'
81 
82 
83 /*
84  * levels of the game; the total number of squares (x * y) must be
85  * relative prime to 17, y may not be larger than Y_SIZE - 4, x may not
86  * be larger then XSIZE - 2
87  */
88 static struct {
89 	char x, y;
90 	unsigned char mines;
91 } levels[5] = {
92 	{ 10, 10, 20 },
93 	{ 20, 10, 40 },
94 	{ 30, 15, 90 },
95 	{ 40, 15, 120 },
96 	{ 60, 20, 240 }
97 };
98 
99 
100 /*
101  * global variables
102  */
103 /*
104  * seed of the pseudo random number generator
105  */
106 static unsigned long random_seed = 0;
107 /*
108  * current level of the game: board dimensions, number of mines
109  */
110 static char x_size, y_size;
111 static unsigned char nr_mines;
112 /*
113  * offsets of the board on screen
114  */
115 static char x_offset = 0, y_offset = 0;
116 /*
117  * termination flag: mine hit
118  */
119 static char mine_hit;
120 /*
121  * number of remaining unchecked fields
122  */
123 static int unchecked;
124 /*
125  * one past the maximal legal index of board
126  */
127 static int l_field;
128 /*
129  * board flags
130  */
131 #define M_MINE 0x80
132 #define M_FLAG 0x40
133 #define M_OPEN 0x20
134 #define M_NUMBER 0x0f
135 static unsigned char field[(Y_SIZE - 4) * (X_SIZE - 2)];
136 
137 
138 /*
139  * macros describing the lines above and below the framed board
140  */
141 #define ABOVE (y_offset - 2)
142 #define BELOW (y_offset + y_size + 1)
143 
144 
145 /*
146  * return a pseudo random number in the range [0..32767]
147  */
148 static int
rand(void)149 rand(void) {
150 	random_seed  = random_seed * 1103515245 + 12345;
151 	return (int) ((random_seed >> 16) & 0x7fff);
152 }
153 
154 
155 /*
156  * print a single character on the console
157  */
158 static void
pchar(char c)159 pchar(char c) { bios(CONOUT, c); }
160 
161 
162 /*
163  * print a null terminated string on the console
164  */
165 static void
pstr(char * cp)166 pstr(char *cp) { while (*cp) bios(CONOUT, *cp++); }
167 
168 
169 /*
170  * display a unsigned byte as decimal number on the console
171  */
172 static void
pubyte(unsigned char uc)173 pubyte(unsigned char uc) {
174 	pchar(uc / 100 + '0');
175 	pchar(uc % 100 / 10 + '0');
176 	pchar(uc % 10 + '0');
177 }
178 
179 
180 /*
181  * turn bold reverse video on and off
182  */
183 
184 static void
br_on(void)185 br_on(void) { pstr("\33e\33i"); }
186 
187 static void
br_off(void)188 br_off(void) { pstr("\33j\33f"); }
189 
190 
191 /*
192  * turn bold video on and off
193  */
194 
195 static void
b_on(void)196 b_on(void) { pstr("\33e"); }
197 
198 static void
b_off(void)199 b_off(void) { pstr("\33f"); }
200 
201 
202 /*
203  * turn graphic characters on and off
204  */
205 
206 static void
graph_on(void)207 graph_on(void) { pstr("\33F"); }
208 
209 static
graph_off(void)210 void graph_off(void) { pstr("\33G"); }
211 
212 
213 /*
214  * clear screen
215  */
216 static void
clear(void)217 clear(void) { pstr("\33E"); }
218 
219 
220 /*
221  * clear to end of line
222  */
223 static void
clear_eoln(void)224 clear_eoln(void) { pstr("\33K"); }
225 
226 
227 /*
228  * read a character from the console
229  */
230 static char
gchar(void)231 gchar(void) { return bios(CONIN); }
232 
233 
234 /*
235  * position cursor to (x, y)
236  */
237 static void
goto_xy(char x,char y)238 goto_xy(char x, char y) {
239 	pstr("\33Y");
240 	pchar(y + ' ');
241 	pchar(x + ' ');
242 }
243 
244 
245 /*
246  * display x_size characters c on the console
247  */
248 static void
hline(char c)249 hline(char c) {
250 	char i;
251 	for (i = 0; i < x_size; i++) pchar(c);
252 }
253 
254 
255 /*
256  * check if the square (x, y) contains a mine
257  */
258 static unsigned char
is_mine(char x,char y)259 is_mine(char x, char y) {
260 	if (x < 0 || x > x_size - 1 || y < 0 || y > y_size - 1) return 0;
261 	return((field[x_size * y + x] & M_MINE) == M_MINE);
262 }
263 
264 
265 /*
266  * show the square (x, y); square must not contain a mine; if square has
267  * no neighbouring mines, recursively open all adjacent unflagged squares
268  */
269 static void
open_field(char x,char y)270 open_field(char x, char y) {
271 	int i;
272 	char n;
273 	/*
274 	 * skip square out of range
275 	 */
276 	if (x < 0 || x > x_size - 1 || y < 0 || y > y_size - 1) return;
277 	/*
278 	 * skip flagged or already opened square
279 	 */
280 	i = x_size * y + x;
281 	if (field[i] & (M_OPEN | M_FLAG)) return;
282 	/*
283 	 * mark square as opened, decrease number of unopened squares
284 	 */
285 	field[i] |= M_OPEN;
286 	unchecked--;
287 	/*
288 	 * get and display number of neighbouring mines
289 	 */
290 	n = field[i] & M_NUMBER;
291 	goto_xy(x + x_offset, y + y_offset);
292 	if (n) {
293 		pchar(n + '0');
294 		return;
295 	}
296 	/*
297 	 * if the square has no neighbouring mines, try to open all neighbours
298 	 */
299 	pchar(' ');
300 	open_field(x - 1, y - 1);
301 	open_field(x, y - 1);
302 	open_field(x + 1, y - 1);
303 	open_field(x - 1, y);
304 	open_field(x + 1, y);
305 	open_field(x - 1, y + 1);
306 	open_field(x, y + 1);
307 	open_field(x + 1, y + 1);
308 }
309 
310 
311 /*
312  * test square (x, y); if there is a mine, the game is lost, if not,
313  * the square is opened
314  */
315 static void
touch_field(char x,char y)316 touch_field(char x, char y) {
317 	int i;
318 	/*
319 	 * skip square out of range
320 	 */
321 	if (x < 0 || x > x_size - 1 || y < 0 || y > y_size - 1) return;
322 	i = x_size * y + x;
323 	/*
324 	 * skip opened or flagged square
325 	 */
326 	if (field[i] & (M_OPEN | M_FLAG)) return;
327 	/*
328 	 * if there is a mine, it explodes and the game is lost
329 	 */
330 	if (field[i] & M_MINE) {
331 		mine_hit = 1;
332 		return;
333 	}
334 	/*
335 	 * otherwise, open the square (and potentially, its neighbours
336 	 */
337 	open_field(x, y);
338 }
339 
340 
341 /*
342  * play a game of Mine Disposal
343  */
344 static void
play(void)345 play(void) {
346 	/*
347 	 * message strings for centering
348 	 */
349 	static char you_win[] = "You win!";
350 	static char you_lose[] = "You lose!";
351 	static char mines[] = "Mines:";
352 	char x, y, key, mines_x;
353 	unsigned char n, mines_left, old_left;
354 	int i;
355 	/*
356 	 * number of mines not flagged
357 	 */
358 	mines_left = old_left = nr_mines;
359 	/*
360 	 * clear the screen and paint the board
361 	 */
362 	clear();
363 	goto_xy(x_offset - 1, y_offset - 1);
364 	graph_on();
365 	pchar('l');
366 	hline('q');
367 	pchar('k');
368 	for (y = 0; y < y_size; y++) {
369 		goto_xy(x_offset - 1, y_offset + y);
370 		pchar('x');
371 		br_on();
372 		hline(' ');
373 		br_off();
374 		pchar('x');
375 	}
376 	goto_xy(x_offset - 1, y_offset + y_size);
377 	pchar('m');
378 	hline('q');
379 	pchar('j');
380 	graph_off();
381 	/*
382 	 * display number of unflagged mines
383 	 */
384 	mines_x = (X_SIZE - sizeof mines - 2) / 2;
385 	goto_xy(mines_x, ABOVE);
386 	pstr(mines);
387 	mines_x += sizeof mines;
388 	goto_xy(mines_x, ABOVE);
389 	pubyte(mines_left);
390 	/*
391 	 * initialize the board
392 	 */
393 	for (i = 0; i < l_field; i++) field[i] = 0;
394 	/*
395 	 * randomly set the mines
396 	 */
397 	for (n = 0; n < nr_mines; n++) {
398 		i = rand() % l_field;
399 		while (field[i] & M_MINE) {
400 			i += 17;
401 			if (i >= l_field) i -= l_field;
402 		}
403 		field[i] |= M_MINE;
404 	}
405 	/*
406 	 * calculate the number of neighbouring mines for all other squares
407 	 */
408 	for (y = 0; y < y_size; y++) {
409 		for (x = 0; x < x_size; x++) {
410 			i = x_size * y + x;
411 			if (field[i] & M_MINE) continue;
412 			n = is_mine(x - 1, y - 1);
413 			n += is_mine(x, y - 1);
414 			n += is_mine(x + 1, y - 1);
415 			n += is_mine(x - 1, y);
416 			n += is_mine(x + 1, y);
417 			n += is_mine(x - 1, y + 1);
418 			n += is_mine(x, y + 1);
419 			n += is_mine(x + 1, y + 1);
420 			field[i] = n;
421 		}
422 	}
423 	/*
424 	 * main loop is performed until all unmined squares have been opened
425 	 * or a mine has been hit
426 	 */
427 	x = y = 0;
428 	unchecked = l_field - nr_mines;
429 	mine_hit = 0;
430 	while (unchecked && ! mine_hit) {
431 		/*
432 		 * display number of unflagged mines, if it has changed
433 		 */
434 		if (mines_left != old_left) {
435 			goto_xy(mines_x, ABOVE);
436 			clear_eoln();
437 			pubyte(mines_left);
438 			old_left = mines_left;
439 		}
440 		/*
441 		 * get a character from the current position;
442 		 * if it is escape, it is a cursor/function key
443 		 */
444 		goto_xy(x + x_offset, y + y_offset);
445 		key = gchar();
446 		if (key == KEY_ESC) {
447 			key = gchar();
448 			switch (key) {
449 			case 'A': key = KEY_CTRLE; break;
450 			case 'B': key = KEY_CTRLX; break;
451 			case 'C': key = KEY_CTRLD; break;
452 			case 'D': key = KEY_CTRLS; break;
453 			case 'P': key = ' '; break;
454 			case 'Q': key = KEY_TAB; break;
455 			case 'R': key = KEY_CR; break;
456 			default: continue;
457 			}
458 		}
459 		switch (key) {
460 		case KEY_CTRLE:
461 			/*
462 			 * cursor up
463 			 */
464 			if (y > 0) y--;
465 			continue;
466 		case KEY_CTRLX:
467 			/*
468 			 * cursor down
469 			 */
470 			if (y < y_size - 1) y++;
471 			continue;
472 		case KEY_CTRLS:
473 			/*
474 			 * cursor left
475 			 */
476 			if (x > 0) x--;
477 			continue;
478 		case KEY_CTRLD:
479 			/*
480 			 * cursor right
481 			 */
482 			if (x < x_size - 1) x++;
483 			continue;
484 		case KEY_CR:
485 			/*
486 			 * set/reset flag
487 			 */
488 			i = x_size * y + x;
489 			if (field[i] & M_OPEN) continue;
490 			br_on();
491 			if (field[i] & M_FLAG) {
492 				field[i] &= ~M_FLAG;
493 				pchar(' ');
494 				mines_left++;
495 			} else {
496 				field[i] |= M_FLAG;
497 				pchar(FLAG);
498 				mines_left--;
499 			}
500 			br_off();
501 			continue;
502 		case KEY_TAB:
503 			/*
504 			 * hit all adjacent squares, unless flagged
505 			 */
506 			i = x_size * y + x;
507 			if (! (field[i] & M_OPEN)) continue;
508 			if (! field[i]) continue;
509 			touch_field(x - 1, y - 1);
510 			touch_field(x, y - 1);
511 			touch_field(x + 1, y - 1);
512 			touch_field(x - 1, y);
513 			touch_field(x + 1, y);
514 			touch_field(x - 1, y + 1);
515 			touch_field(x, y + 1);
516 			touch_field(x + 1, y + 1);
517 			continue;
518 		case ' ':
519 			/*
520 			 * hit field
521 			 */
522 			touch_field(x, y);
523 			continue;
524 		}
525 	}
526 	/*
527 	 * display win or lose message
528 	 */
529 	goto_xy(0, ABOVE);
530 	clear_eoln();
531 	goto_xy((X_SIZE - sizeof you_win + 1) / 2, ABOVE);
532 	b_on();
533 	pstr(mine_hit ? you_lose : you_win);
534 	b_off();
535 	/*
536 	 * show all falsely flagged squares or unflagged mines
537 	 */
538 	i = 0;
539 	for (y = 0; y < y_size; y++) {
540 		for (x = 0; x < x_size; x++) {
541 			if (field[i] & M_FLAG) {
542 				if (! (field[i] & M_MINE)) {
543 					goto_xy(x + x_offset, y + y_offset);
544 					br_on();
545 					pchar(WRONG);
546 					br_off();
547 				}
548 			} else if (field[i] & M_MINE) {
549 				/*
550 				 * in case of loss, show all undiscovered
551 				 * mines; in case of a win (i. e., if all
552 				 * remaining unopened fields contain mines),
553 				 * show them as flagged
554 				 */
555 				goto_xy(x + x_offset, y + y_offset);
556 				br_on();
557 				pchar(mine_hit ? MINE : FLAG);
558 				br_off();
559 			}
560 			i++;
561 		}
562 	}
563 }
564 
565 
566 void
main(void)567 main(void) {
568 	/*
569 	 * message string for centering
570 	 */
571 	static char again[] = "Play again (y/n): ";
572 	char key, level;
573 	/*
574 	 * clear screen and show title
575 	 */
576 	clear();
577 	b_on();
578 	pstr("Mine Disposal");
579 	b_off();
580 	/*
581 	 * query level; the time to the keypress of the user's answer
582 	 * is used to initialize the pseudo random number generator
583 	 */
584 	pstr("\r\n\nEnter level (1-5): ");
585 	while (! bios(CONST)) random_seed++;
586 	do {
587 		key = gchar();
588 	} while (key < '1' || key > '5');
589 	pchar(key);
590 	level = key - '1';
591 	/*
592 	 * get parameters of selected level
593 	 */
594 	x_size = levels[level].x;
595 	y_size = levels[level].y;
596 	nr_mines = levels[level].mines;
597 	/*
598 	 * calculate board limit
599 	 */
600 	l_field = (int) x_size * (int) y_size;
601 	/*
602 	 * calculate board offset on screen
603 	 */
604 	x_offset = (X_SIZE - x_size) / 2;
605 	y_offset = (Y_SIZE - y_size) / 2;
606 	/*
607 	 * repeat game until user doesn't want to play again
608 	 */
609 	do {
610 		play();
611 		goto_xy((X_SIZE - sizeof again) / 2, BELOW);
612 		pstr(again);
613 		do {
614 			key = gchar();
615 		} while (key != 'N' && key != 'n' && key != 'y' && key != 'Y');
616 		pchar(key);
617 	} while (key == 'Y' || key == 'y');
618 	/*
619 	 * exit game
620 	 */
621 	goto_xy(0, Y_SIZE - 1);
622 	bios(WBOOT);
623 }
624