1 /*
2  * nsnake.c -- a snake game for your terminal
3  *
4  * Copyright (c) 2011-2019 David Demelier <markand@malikania.fr>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  */
18 
19 #include <sys/stat.h>
20 #include <signal.h>
21 #include <stdio.h>
22 #include <stdlib.h>
23 #include <stdint.h>
24 #include <string.h>
25 #include <time.h>
26 
27 #include <curses.h>
28 
29 #if !defined(_WIN32)
30 #	include <unistd.h>
31 #	include <sys/types.h>
32 #	include <pwd.h>
33 #else
34 #	include <io.h>
35 #	include <lmcons.h>
36 #	include <windows.h>
37 #endif
38 
39 #include "sysconfig.h"
40 
41 #if !defined(HAVE_RANDOM)
42 #	define random rand
43 #	define srandom srand
44 #endif
45 
46 #if defined(HAVE_ERR)
47 #	include <err.h>
48 #else
49 #	include "extern/err.c"
50 #endif
51 
52 #if !defined(HAVE_GETOPT)
53 #	include "extern/getopt.c"
54 #endif
55 
56 #define HEIGHT          23
57 #define WIDTH           78
58 #define SIZE            ((HEIGHT - 2) * (WIDTH - 2))
59 #define DATABASE        VARDIR "/db/nsnake/scores"
60 
61 enum grid {
62 	GRID_EMPTY,
63 	GRID_WALL,
64 	GRID_SNAKE,
65 	GRID_FOOD
66 };
67 
68 struct snake {
69 	uint32_t score;         /* user score */
70 	uint16_t length;        /* snake's size */
71 	int8_t dirx;            /* direction in x could be 0, 1 or -1 */
72 	int8_t diry;            /* same for y */
73 
74 	struct {
75 		uint8_t x;      /* each snake part has (x, y) position */
76 		uint8_t y;      /* each part will be displayed */
77 	} pos[SIZE];
78 };
79 
80 struct food {
81 	enum {
82 		NORM = 0,       /* both increase the score but only NORM */
83 		FREE            /* increase the snake's length too */
84 	} type;
85 
86 	uint8_t x;              /* Position of the current food, will be used */
87 	uint8_t y;              /* in grid[][]. */
88 };
89 
90 struct score {
91 #if defined(_WIN32)
92 #	define NAMELEN UNLEN
93 #else
94 #	define NAMELEN 32
95 #endif
96 	char name[NAMELEN + 1]; /* highscore's name */
97 	uint32_t score;         /* score */
98 	time_t time;            /* when? */
99 	uint8_t wc;             /* wallcrossing or not */
100 };
101 
102 static int setcolors = 1;       /* enable colors */
103 static int warp = 1;            /* enable wall crossing */
104 static int color = 2;           /* green color by default */
105 static int noscore = 0;         /* do not score */
106 static int verbose = 0;         /* be verbose */
107 
108 static uint8_t grid[HEIGHT][WIDTH] = {{ GRID_EMPTY }};
109 static WINDOW *top = NULL;
110 static WINDOW *frame = NULL;
111 
112 static void
wset(WINDOW * frame,int pair)113 wset(WINDOW *frame, int pair)
114 {
115 	if (setcolors)
116 		wattron(frame, pair);
117 }
118 
119 static void
wunset(WINDOW * frame,int pair)120 wunset(WINDOW *frame, int pair)
121 {
122 	if (setcolors)
123 		wattroff(frame, pair);
124 }
125 
126 static void
repaint(void)127 repaint(void)
128 {
129 	refresh();
130 	wrefresh(top);
131 	wrefresh(frame);
132 }
133 
134 static int
init(void)135 init(void)
136 {
137 	initscr();
138 	noecho();
139 	curs_set(0);
140 	keypad(stdscr, TRUE);
141 	nodelay(stdscr, TRUE);
142 
143 	if (COLS < (WIDTH + 1) || LINES < (HEIGHT + 1))
144 		return -1;
145 
146 	if (color < 0 || color > 8)
147 		color = 4;
148 	else
149 		color += 2;
150 
151 	if (setcolors && has_colors()) {
152 		int i;
153 
154 		use_default_colors();
155 		start_color();
156 
157 		init_pair(0, COLOR_WHITE, COLOR_BLACK); /* topbar */
158 		init_pair(1, COLOR_YELLOW, -1);         /* food */
159 
160 		for (i = 0; i < COLORS; ++i)
161 			init_pair(i + 2, i, -1);
162 	}
163 
164 	top = newwin(1, 0, 0, 0);
165 	frame = newwin(HEIGHT, WIDTH, (LINES/2)-(HEIGHT/2), (COLS/2)-(WIDTH/2));
166 	box(frame, ACS_VLINE, ACS_HLINE);
167 
168 	if (setcolors) {
169 		wbkgd(top, COLOR_PAIR(0));
170 		wattrset(top, COLOR_PAIR(0) | A_BOLD);
171 	}
172 
173 	repaint();
174 
175 	return 1;
176 }
177 
178 static void
set_grid(struct snake * sn)179 set_grid(struct snake *sn)
180 {
181 	uint16_t i;
182 
183 	for (i = 0; i < sn->length; ++i)
184 		grid[sn->pos[i].y][sn->pos[i].x] = GRID_SNAKE;
185 
186 	/*
187 	 * each snake part must follow the last part, pos[0] is head, then
188 	 * pos[2] takes pos[1] place, pos[3] takes pos[2] and so on.
189 	 */
190 	grid[sn->pos[sn->length-1].y][sn->pos[sn->length-1].x] = GRID_EMPTY;
191 	memmove(&sn->pos[1], &sn->pos[0], sizeof (sn->pos) - sizeof (sn->pos[0]));
192 }
193 
194 static void
draw(const struct snake * sn,const struct food * fd)195 draw(const struct snake *sn, const struct food *fd)
196 {
197 	uint16_t i;
198 
199 	for (i = 0; i < sn->length; ++i) {
200 		wset(frame, COLOR_PAIR(color));
201 		mvwaddch(frame, sn->pos[i].y, sn->pos[i].x, '#');
202 		wunset(frame, COLOR_PAIR(color));
203 	}
204 
205 	/* Print head */
206 	wset(frame, COLOR_PAIR(color) | A_BOLD);
207 	mvwaddch(frame, sn->pos[0].y, sn->pos[0].x, '@');
208 	wunset(frame, COLOR_PAIR(color) | A_BOLD);
209 
210 	/* Erase the snake's tail */
211 	mvwaddch(frame, sn->pos[sn->length].y, sn->pos[sn->length].x, ' ');
212 
213 	/* Print food */
214 	wset(frame, COLOR_PAIR(1) | A_BOLD);
215 	mvwaddch(frame, fd->y, fd->x, (fd->type == FREE) ? '*' : '+');
216 	wunset(frame, COLOR_PAIR(1) | A_BOLD);
217 
218 	/* Print score */
219 	wmove(top, 0, 0);
220 	wprintw(top, "Score: %d", sn->score);
221 	repaint();
222 }
223 
224 static int
is_dead(const struct snake * sn)225 is_dead(const struct snake *sn)
226 {
227 	if (grid[sn->pos[0].y][sn->pos[0].x] == GRID_SNAKE)
228 		return 1;
229 
230 	/* No warp enabled means dead in wall */
231 	return !warp && grid[sn->pos[0].y][sn->pos[0].x] == GRID_WALL;
232 }
233 
234 static int
is_eaten(const struct snake * sn)235 is_eaten(const struct snake *sn)
236 {
237 	return grid[sn->pos[0].y][sn->pos[0].x] == GRID_FOOD;
238 }
239 
240 static void
spawn(struct food * fd)241 spawn(struct food *fd)
242 {
243 	int num;
244 
245 	do {
246 		fd->x = (random() % (WIDTH - 2)) + 1;
247 		fd->y = (random() % (HEIGHT - 2)) + 1;
248 	} while (grid[fd->y][fd->x] != GRID_EMPTY);
249 
250 	/* Free food does not grow the snake */
251 	num = ((random() % 7) - 1) + 1;
252 	fd->type = (num == 6) ? FREE : NORM;
253 }
254 
255 static void
direction(struct snake * sn,int ch)256 direction(struct snake *sn, int ch)
257 {
258 	switch (ch) {
259 	case KEY_LEFT: case 'h': case 'H':
260 		if (sn->dirx != 1) {
261 			sn->dirx = -1;
262 			sn->diry = 0;
263 		}
264 
265 		break;
266 	case KEY_UP: case 'k': case 'K':
267 		if (sn->diry != 1) {
268 			sn->dirx = 0;
269 			sn->diry = -1;
270 		}
271 
272 		break;
273 	case KEY_DOWN: case 'j': case 'J':
274 		if (sn->diry != -1) {
275 			sn->dirx = 0;
276 			sn->diry = 1;
277 		}
278 
279 		break;
280 	case KEY_RIGHT: case 'l': case 'L':
281 		if (sn->dirx != -1) {
282 			sn->dirx = 1;
283 			sn->diry = 0;
284 		}
285 
286 		break;
287 	default:
288 		break;
289 	}
290 }
291 
292 static int
init_score(const struct score * sc)293 init_score(const struct score *sc)
294 {
295 	FILE *fp;
296 	char header[12] = "nsnake-score";
297 	uint32_t nscore = 1;
298 
299 	if (!(fp = fopen(DATABASE, "w+b")))
300 		return 0;
301 
302 	fwrite(header, sizeof (header), 1, fp);
303 	fwrite(&nscore, sizeof (nscore), 1, fp);
304 	fwrite(sc, sizeof (*sc), 1, fp);
305 	fclose(fp);
306 
307 	return 1;
308 }
309 
310 static int
insert_score(const struct score * sc)311 insert_score(const struct score *sc)
312 {
313 	FILE *fp;
314 	uint32_t nscore, i;
315 	char header[12] = { 0 };
316 	struct score *buffer;
317 
318 	if (!(fp = fopen(DATABASE, "r+b")))
319 		return 0;
320 
321 	fread(header, sizeof (header), 1, fp);
322 	if (strncmp(header, "nsnake-score", sizeof (header)) != 0) {
323 		fclose(fp);
324 		return 0;
325 	}
326 
327 	fread(&nscore, sizeof (nscore), 1, fp);
328 	if (!(buffer = calloc(nscore + 1, sizeof (*buffer)))) {
329 		fclose(fp);
330 		return 0;
331 	}
332 
333 	fread(buffer, sizeof (*buffer), nscore, fp);
334 	for (i = 0; i < nscore; ++i)
335 		if (sc->score >= buffer[i].score && sc->wc == buffer[i].wc)
336 			break;
337 
338 	/* Replace same score */
339 	if (sc->score == buffer[i].score && strcmp(buffer[i].name, sc->name) == 0)
340 		memcpy(&buffer[i], sc, sizeof (*sc));
341 	else {
342 		memmove(&buffer[i + 1], &buffer[i], (sizeof (*sc)) * (nscore - i));
343 		memcpy(&buffer[i], sc, sizeof (*sc));
344 
345 		/* There is now a new entry */
346 		++ nscore;
347 
348 		/* Update number of score entries */
349 		fseek(fp, sizeof (header), SEEK_SET);
350 		fwrite(&nscore, sizeof (nscore), 1, fp);
351 	}
352 
353 	/* Finally write */
354 	fseek(fp, sizeof (header) + sizeof (nscore), SEEK_SET);
355 	fwrite(buffer, sizeof (*sc), nscore, fp);
356 	free(buffer);
357 	fclose (fp);
358 
359 	return 1;
360 }
361 
362 static int
register_score(const struct snake * sn)363 register_score(const struct snake *sn)
364 {
365 	struct score sc;
366 	struct stat st;
367 	int (*reshandler)(const struct score *);
368 
369 	memset(&sc, 0, sizeof (sc));
370 
371 #if defined(_WIN32)
372 	DWORD length = NAMELEN + 1;
373 	GetUserNameA(sc.name, &length);
374 #else
375 	strncpy(sc.name, getpwuid(getuid())->pw_name, sizeof (sc.name));
376 #endif
377 	sc.score = sn->score;
378 	sc.time = time(NULL);
379 	sc.wc = warp;
380 
381 	if (stat(DATABASE, &st) < 0 || st.st_size == 0)
382 		reshandler = &(init_score);
383 	else
384 		reshandler = &(insert_score);
385 
386 	return reshandler(&sc);
387 }
388 
389 static void
show_scores(void)390 show_scores(void)
391 {
392 	FILE *fp;
393 	uint32_t nscore, i;
394 	char header[12] = { 0 };
395 	struct score sc;
396 
397 	if (!(fp = fopen(DATABASE, "rb")))
398 		err(1, "Could not read %s", DATABASE);
399 
400 	if (verbose)
401 		printf("Wall crossing %s\n", (warp) ? "enabled" : "disabled");
402 
403 	fread(header, sizeof (header), 1, fp);
404 	if (strncmp(header, "nsnake-score", sizeof (header)) != 0)
405 		errx(1, "Not a valid nsnake score file");
406 
407 	fread(&nscore, sizeof (nscore), 1, fp);
408 	for (i = 0; i < nscore; ++i) {
409 		fread(&sc, sizeof (sc), 1, fp);
410 
411 		if (sc.wc == warp) {
412 			char date[128] = { 0 };
413 			struct tm *tm = localtime(&sc.time);
414 
415 			strftime(date, sizeof (date), "%c", tm);
416 			printf("%-16s%-10u %s\n", sc.name, sc.score, date);
417 		}
418 	}
419 
420 	fclose(fp);
421 	exit(0);
422 }
423 
424 static void
wait(unsigned ms)425 wait(unsigned ms)
426 {
427 #if defined(_WIN32)
428 	Sleep(ms);
429 #else
430 	struct timespec ts = {
431 		.tv_sec = 0,
432 		.tv_nsec = ms * 1000000
433 	};
434 
435 	nanosleep(&ts, NULL);
436 #endif
437 }
438 
439 static void
quit(const struct snake * sn)440 quit(const struct snake *sn)
441 {
442 	uint16_t i;
443 
444 	if (sn != NULL) {
445 		for (i = 0; i < sn->length; ++i) {
446 			mvwaddch(frame, sn->pos[i].y, sn->pos[i].x, ' ');
447 			wait(50);
448 			repaint();
449 		}
450 	}
451 
452 	delwin(top);
453 	delwin(frame);
454 	delwin(stdscr);
455 	endwin();
456 }
457 
458 #if defined(HAVE_SIGWINCH)
459 
460 static void
resize_handler(int signal)461 resize_handler(int signal)
462 {
463 	int x, y;
464 
465 	if (signal != SIGWINCH)
466 		return;
467 
468 	/* XXX: I would like to pause the game until the terminal is resized */
469 	endwin();
470 
471 #if defined(HAVE_RESIZETERM)
472 	resizeterm(LINES, COLS);
473 #endif
474 	repaint(); clear();
475 
476 	/* Color the top bar */
477 	wbkgd(top, COLOR_PAIR(0) | A_BOLD);
478 
479 	getmaxyx(stdscr, y, x);
480 
481 	if (x < WIDTH || y < HEIGHT) {
482 		quit(NULL);
483 		errx(1, "Terminal has been resized too small, aborting");
484 	}
485 
486 	mvwin(frame, (y / 2) - (HEIGHT / 2), (x / 2) - (WIDTH / 2));
487 	repaint();
488 }
489 
490 #endif
491 
492 static void
usage(void)493 usage(void)
494 {
495 	fprintf(stderr, "usage: nsnake [-cnsvw] [-C color]\n");
496 	exit(1);
497 }
498 
499 int
main(int argc,char * argv[])500 main(int argc, char *argv[])
501 {
502 	int running;
503 	int x, y, ch;
504 	int showscore = 0;
505 
506 	struct snake sn = { 0, 4, 1, 0, {
507 		{5, 10}, {5, 9}, {5, 8}, {5, 7} }
508 	};
509 
510 	struct food fd = { NORM, 0, 0 };
511 
512 	while ((ch = getopt(argc, argv, "cC:nsvw")) != -1) {
513 		switch (ch) {
514 		case 'c':
515 			setcolors = 0;
516 			break;
517 		case 'C':
518 			color = atoi(optarg);
519 			break;
520 		case 'n':
521 			noscore = 1;
522 			break;
523 		case 's':
524 			showscore = 1;
525 			break;
526 		case 'v':
527 			verbose = 1;
528 			break;
529 		case 'w':
530 			warp = 0;
531 			break;
532 		default:
533 			usage();
534 			/* NOTREACHED */
535 		}
536 	}
537 
538 	if (showscore)
539 		show_scores();
540 
541 	argc -= optind;
542 	argv += optind;
543 
544 	srandom((unsigned)time(NULL));
545 
546 #if defined(HAVE_SIGWINCH)
547 	signal(SIGWINCH, resize_handler);
548 #endif
549 
550 	if (!init()) {
551 		quit(NULL);
552 		errx(1, "Terminal too small, aborting");
553 	}
554 
555 	if (top == NULL || frame == NULL) {
556 		endwin();
557 		errx(1, "ncurses failed to init");
558 	}
559 
560 	/* Apply GRID_WALL to the edges */
561 	for (y = 0; y < HEIGHT; ++y)
562 		grid[y][0] = grid[y][WIDTH - 1] = GRID_WALL;
563 	for (x = 0; x < WIDTH; ++x)
564 		grid[0][x] = grid[HEIGHT - 1][x] = GRID_WALL;
565 
566 	/* Do not spawn food on snake */
567 	set_grid(&sn);
568 	spawn(&fd);
569 
570 	/* Apply food on the grid */
571 	grid[fd.y][fd.x] = GRID_FOOD;
572 	draw(&sn, &fd);
573 
574 	/* First direction is to right */
575 	sn.pos[0].x += sn.dirx;
576 
577 	running = 1;
578 	while (!is_dead(&sn) && running) {
579 		if (is_eaten(&sn)) {
580 			int i;
581 
582 			if (fd.type == NORM)
583 				sn.length += 2;
584 
585 			for (i = 0; i < sn.length; ++i)
586 				grid[sn.pos[i].y][sn.pos[i].x] = GRID_SNAKE;
587 
588 			/* If the screen is totally filled */
589 			if (sn.length >= SIZE) {
590 				/* Emulate new game */
591 				for (i = 4; i < SIZE; ++i) {
592 					mvwaddch(frame, sn.pos[i].y, sn.pos[i].x, ' ');
593 					grid[sn.pos[i].y][sn.pos[i].x] = GRID_EMPTY;
594 				}
595 
596 				sn.length = 4;
597 			}
598 
599 			if (fd.type == NORM)
600 				set_grid(&sn);
601 
602 			/* Prevent food spawning on snake's tail */
603 			spawn(&fd);
604 
605 			sn.score += 1;
606 			grid[fd.y][fd.x] = GRID_FOOD;
607 		}
608 
609 		/* Draw and define grid with snake parts */
610 		draw(&sn, &fd);
611 		set_grid(&sn);
612 
613 		/* Go to the next position */
614 		sn.pos[0].x += sn.dirx;
615 		sn.pos[0].y += sn.diry;
616 
617 		ch = getch();
618 		if (ch == 'p') {
619 			nodelay(stdscr, FALSE);
620 			while ((ch = getch()) != 'p' && ch != 'q')
621 				;
622 
623 			if (ch == 'q')
624 				running = 0;
625 
626 			nodelay(stdscr, TRUE);
627 		} else if (ch == 'q')
628 			running = 0;
629 		else if (ch == 'c')
630 			color = (color + 1) % 8;
631 		else if (ch)
632 			direction(&sn, ch);
633 
634 		/* If warp enabled, touching wall cross to the opposite */
635 		if (warp) {
636 			if (sn.pos[0].x == WIDTH - 1)
637 				sn.pos[0].x = 1;
638 			else if (sn.pos[0].x == 0)
639 				sn.pos[0].x = WIDTH - 2;
640 			else if (sn.pos[0].y == HEIGHT - 1)
641 				sn.pos[0].y = 1;
642 			else if (sn.pos[0].y == 0)
643 				sn.pos[0].y = HEIGHT - 2;
644 		}
645 
646 		if (sn.diry != 0)
647 			wait(118);
648 		else
649 			wait(100);
650 	}
651 
652 	/* The snake is dead. */
653 	quit(&sn);
654 
655 	/* User has left or is he dead? */
656 	printf("%sScore: %d\n", is_dead(&sn) ? "You died...\n" : "", sn.score);
657 
658 	if (!noscore && !register_score(&sn))
659 		err(1, "Could not write score file %s", DATABASE);
660 
661 	return 0;
662 }
663