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