1 /*
2 * greed.c - Written by Matthew T. Day (mday@iconsys.uu.net), 09/06/89
3 *
4 * Now maintained by Eric S. Raymond <esr@snark.thyrsus.com>. Matt
5 * Day dropped out of sight and hasn't posted a new version or patch
6 * in many years.
7 *
8 * 11/15/95 Fred C. Smith (Yes, the SAME Fred C. Smith who did the MS-DOS
9 * version), fix the 'p' option so when it removes the highlight
10 * from unused possible moves, it restores the previous color.
11 * -Some minor changes in the way it behaves at the end of a game,
12 * because I didn't like the way someone had changed it to work
13 * since I saw it a few years ago (personal preference).
14 * -Some style changes in the code, again personal preference.
15 * fredex@fcshome.stoneham.ma.us
16 */
17
18 /*
19 * When using a curses library with color capability, Greed will
20 * detect color curses(3) if you have it and generate the board in
21 * color, one color to each of the digit values. This will also enable
22 * checking of an environment variable GREEDOPTS to override the
23 * default color set, which will be parsed as a string of the form:
24 *
25 * <c1><c2><c3><c4><c5><c6><c7><c8><c9>[:[p]]
26 *
27 * where <cn> is a character decribing the color for digit n.
28 * The color letters are read as follows:
29 * b = blue,
30 * g = green,
31 * c = cyan,
32 * r = red,
33 * m = magenta,
34 * y = yellow,
35 * w = white.
36 * In addition, capitalizing a letter turns on the A_BOLD attribute for that
37 * letter.
38 *
39 * If the string ends with a trailing :, letters following are taken as game
40 * options. At present, only 'p' (equivalent to an initial 'p' command) is
41 * defined.
42 *
43 * SPDX-License-Identifier: BSD-2-Clause
44 */
45
46 static char *version = "Greed v" RELEASE;
47
48 #include <stdlib.h>
49 #include <unistd.h>
50 #include <string.h>
51 #include <ctype.h>
52 #include <curses.h>
53 #include <term.h>
54 #include <signal.h>
55 #include <pwd.h>
56 #include <fcntl.h>
57 #include <stdbool.h>
58 #include <time.h>
59 #ifdef A_COLOR
60 #include <ctype.h>
61 #endif
62
63 #define HEIGHT 22
64 #define WIDTH 79
65 #define ME '@'
66
67 /*
68 * The scorefile is fixed-length binary and consists of
69 * structure images - very un-Unixy design!
70 */
71 #define MAXSCORES 10
72 #define SCOREFILESIZE (MAXSCORES * sizeof(struct score))
73
74 /* rnd() returns a random number between 1 and x */
75 #define rnd(x) (int) ((lrand48() % (x))+1)
76
77 #define LOCKPATH "/tmp/Greed.lock" /* lock path for high score file */
78
79 #define LOCALSCOREFILE ".greedscores"
80
81 #define USERNAMELEN 32 /* length of leading segment that we keep */
82
83 /*
84 * changing stuff in this struct
85 * makes old score files incompatible
86 */
87 struct score {
88 char user[USERNAMELEN + 1];
89 time_t time;
90 int score;
91 };
92
93 static int grid[HEIGHT][WIDTH], y, x;
94 static bool allmoves = false, havebotmsg = false;
95 static int score = 0;
96 static char *cmdname;
97 static WINDOW *helpwin = NULL;
98
99 static void topscores(int);
100 static int tunnel(chtype, int *);
101 static int othermove(int, int);
102
botmsg(char * msg,bool backcur)103 static void botmsg(char *msg, bool backcur)
104 /*
105 * botmsg() writes "msg" at the middle of the bottom line of the screen.
106 * Boolean "backcur" specifies whether to put cursor back on the grid or
107 * leave it on the bottom line (e.g. for questions).
108 */
109 {
110 mvaddstr(23, 40, msg);
111 clrtoeol();
112 if (backcur)
113 move(y, x);
114 refresh();
115 havebotmsg = true;
116 }
117
118
quit(int sig)119 static void quit(int sig)
120 /*
121 * quit() is run when the user hits ^C or ^\, it queries the user if he
122 * really wanted to quit, and if so, checks the high score stuff (with the
123 * current score) and quits; otherwise, simply returns to the game.
124 */
125 {
126 int ch;
127 void (*osig)() = signal(SIGINT, SIG_IGN); /* save old signal */
128 (void) signal(SIGQUIT, SIG_IGN);
129
130 if (stdscr) {
131 botmsg("Really quit? ", false);
132 if ((ch = getch()) != 'y' && ch != 'Y') {
133 move(y, x);
134 (void) signal(SIGINT, osig); /* reset old signal */
135 (void) signal(SIGQUIT, osig);
136 refresh();
137 return;
138 }
139 move(23, 0);
140 refresh();
141 endwin();
142 puts("\n");
143 topscores(score);
144 }
145 exit(0);
146 }
147
out(int onsig)148 static void out(int onsig)
149 /*
150 * out() is run when the signal SIGTERM is sent, it corrects the terminal
151 * state (if necessary) and exits.
152 */
153 {
154 if (stdscr) endwin();
155 exit(0);
156 }
157
158
usage(void)159 static void usage(void)
160 /* usage() prints out the proper command line usage for Greed and exits. */
161 {
162 fprintf(stderr, "Usage: %s [-p] [-s]\n", cmdname);
163 exit(1);
164 }
165
showscore(void)166 static void showscore(void)
167 /*
168 * showscore() prints the score and the percentage of the screen eaten
169 * at the beginning of the bottom line of the screen, moves the
170 * cursor back on the grid, and refreshes the screen.
171 */
172 {
173 mvprintw(23, 7, "%d %.2f%%",
174 score,
175 (float)(score * 100) / (HEIGHT * WIDTH));
176 move(y, x);
177 refresh();
178 }
179
180 void showmoves(bool, int*);
181
main(int argc,char ** argv)182 int main(int argc, char **argv)
183 {
184 int val = 1;
185 int attribs[9];
186 #ifdef A_COLOR
187 char *colors;
188 #endif
189
190 cmdname = argv[0]; /* save the command name */
191 if (argc == 2) { /* process the command line */
192 if (strlen(argv[1]) != 2 || argv[1][0] != '-') usage();
193 if (argv[1][1] == 's') {
194 topscores(0);
195 exit(0);
196 }
197 }
198 else if (argc > 2) /* can't have > 2 arguments */
199 usage();
200
201 (void) signal(SIGINT, quit); /* catch off the signals */
202 (void) signal(SIGQUIT, quit);
203 (void) signal(SIGTERM, out);
204
205 initscr(); /* set up the terminal modes */
206 #ifdef KEY_MIN
207 keypad(stdscr, true);
208 #endif /* KEY_MIN */
209 cbreak();
210 noecho();
211
212 srand48(time(0) ^ getpid() << 16); /* initialize the random seed *
213 * with a unique number */
214
215 #ifdef A_COLOR
216 if (has_colors()) {
217 start_color();
218 init_pair(1, COLOR_YELLOW, COLOR_BLACK);
219 init_pair(2, COLOR_RED, COLOR_BLACK);
220 init_pair(3, COLOR_GREEN, COLOR_BLACK);
221 init_pair(4, COLOR_CYAN, COLOR_BLACK);
222 init_pair(5, COLOR_MAGENTA, COLOR_BLACK);
223
224 attribs[0] = COLOR_PAIR(1);
225 attribs[1] = COLOR_PAIR(2);
226 attribs[2] = COLOR_PAIR(3);
227 attribs[3] = COLOR_PAIR(4);
228 attribs[4] = COLOR_PAIR(5);
229 attribs[5] = COLOR_PAIR(1) | A_BOLD;
230 attribs[6] = COLOR_PAIR(2) | A_BOLD;
231 attribs[7] = COLOR_PAIR(3) | A_BOLD;
232 attribs[8] = COLOR_PAIR(4) | A_BOLD;
233
234 if ((colors = getenv("GREEDOPTS")) != (char *) NULL) {
235 static char *cnames = " rgybmcwRGYBMCW";
236 char *cp;
237
238 for (cp = colors; *cp && *cp != ':'; cp++)
239 if (strchr(cnames, *cp) != (char *) NULL)
240 if (*cp != ' ') {
241 init_pair(cp-colors+1,
242 strchr(cnames, tolower(*cp))-cnames,
243 COLOR_BLACK);
244 attribs[cp-colors]=COLOR_PAIR(cp-colors+1);
245 if (isupper(*cp))
246 attribs[cp-colors] |= A_BOLD;
247 }
248 if (*cp == ':')
249 while (*++cp)
250 if (*cp == 'p')
251 allmoves = true;
252 }
253 }
254 #endif
255
256 for (y=0; y < HEIGHT; y++) /* fill the grid array and */
257 for (x=0; x < WIDTH; x++) /* print numbers out */
258 #ifdef A_COLOR
259 if (has_colors()) {
260 int newval = rnd(9);
261
262 attron(attribs[newval - 1]);
263 mvaddch(y, x, (grid[y][x] = newval) + '0');
264 attroff(attribs[newval - 1]);
265 } else
266 #endif
267 mvaddch(y, x, (grid[y][x] = rnd(9)) + '0');
268
269 mvaddstr(23, 0, "Score: "); /* initialize bottom line */
270 mvprintw(23, 40, "%s - Hit '?' for help.", version);
271 y = rnd(HEIGHT)-1; x = rnd(WIDTH)-1; /* random initial location */
272 standout();
273 mvaddch(y, x, ME);
274 standend();
275 grid[y][x] = 0; /* eat initial square */
276
277 if (allmoves)
278 showmoves(true, attribs);
279 showscore();
280
281 /* main loop, gives tunnel() a user command */
282 while ((val = tunnel(getch(), attribs)) > 0)
283 continue;
284
285 if (!val) { /* if didn't quit by 'q' cmd */
286 botmsg("Hit any key..", false); /* then let user examine */
287 getch(); /* final screen */
288 }
289
290 move(23, 0);
291 refresh();
292 endwin();
293 puts("\n"); /* writes two newlines */
294 topscores(score);
295 exit(0);
296 }
297
298
tunnel(chtype cmd,int * attribs)299 static int tunnel(chtype cmd, int *attribs)
300 /*
301 * tunnel() does the main game work. Returns 1 if everything's okay, 0 if
302 * user "died", and -1 if user specified and confirmed 'q' (fast quit).
303 */
304 {
305 int dy, dx, distance;
306 void help(void);
307
308 switch (cmd) { /* process user command */
309 case 'h': case 'H': case '4':
310 #ifdef KEY_LEFT
311 case KEY_LEFT:
312 #endif /* KEY_LEFT */
313 dy = 0; dx = -1;
314 break;
315 case 'j': case 'J': case '2':
316 #ifdef KEY_DOWN
317 case KEY_DOWN:
318 #endif /* KEY_DOWN */
319 dy = 1; dx = 0;
320 break;
321 case 'k': case 'K': case '8':
322 #ifdef KEY_UP
323 case KEY_UP:
324 #endif /* KEY_UP */
325 dy = -1; dx = 0;
326 break;
327 case 'l': case 'L': case '6':
328 #ifdef KEY_RIGHT
329 case KEY_RIGHT:
330 #endif /* KEY_RIGHT */
331 dy = 0; dx = 1;
332 break;
333 case 'b': case 'B': case '1':
334 dy = 1; dx = -1;
335 break;
336 case 'n': case 'N': case '3':
337 dy = dx = 1;
338 break;
339 case 'y': case 'Y': case '7':
340 dy = dx = -1;
341 break;
342 case 'u': case 'U': case '9':
343 dy = -1; dx = 1;
344 break;
345 case 'p': case 'P':
346 allmoves = !allmoves;
347 showmoves(allmoves, attribs);
348 move(y, x);
349 refresh();
350 return (1);
351 case 'q': case 'Q':
352 quit(0);
353 return(1);
354 case '?':
355 help();
356 return (1);
357 case '\14': case '\22': /* ^L or ^R (redraw) */
358 wrefresh(curscr); /* falls through to return */
359 default:
360 return (1);
361 }
362 distance = (y+dy >= 0 && x+dx >= 0 && y+dy < HEIGHT && x+dx < WIDTH) ?
363 grid[y+dy][x+dx] : 0;
364
365 {
366 int j = y, i = x, d = distance;
367
368 do { /* process move for validity */
369 j += dy;
370 i += dx;
371 if (j >= 0 && i >= 0 && j < HEIGHT && i < WIDTH && grid[j][i])
372 continue; /* if off the screen */
373 else if (!othermove(dy, dx)) { /* no other good move */
374 j -= dy;
375 i -= dx;
376 mvaddch(y, x, ' ');
377 while (y != j || x != i) {
378 y += dy; /* so we manually */
379 x += dx; /* print chosen path */
380 score++;
381 mvaddch(y, x, ' ');
382 }
383 mvaddch(y, x, '*'); /* with a '*' */
384 showscore(); /* print final score */
385 return (0);
386 } else { /* otherwise prevent bad move */
387 botmsg("Bad move.", true);
388 return (1);
389 }
390 } while (--d);
391 }
392
393 /* remove possible moves */
394 if (allmoves)
395 showmoves(false, attribs);
396
397 if (havebotmsg) { /* if old bottom msg exists */
398 mvprintw(23, 40, "%s - Hit '?' for help.", version);
399 havebotmsg = false;
400 }
401
402 mvaddch(y, x, ' '); /* erase old ME */
403 do { /* print good path */
404 y += dy;
405 x += dx;
406 score++;
407 grid[y][x] = 0;
408 mvaddch(y, x, ' ');
409 } while (--distance);
410 standout();
411 mvaddch(y, x, ME); /* put new ME */
412 standend();
413 if (allmoves)
414 showmoves(true, attribs); /* put new possible moves */
415 showscore(); /* does refresh() finally */
416 return (1);
417 }
418
othermove(int bady,int badx)419 static int othermove(int bady, int badx)
420 /*
421 * othermove() checks area for an existing possible move. bady and
422 * badx are direction variables that tell othermove() they are
423 * already no good, and to not process them. I don't know if this
424 * is efficient, but it works!
425 */
426 {
427 int dy = -1, dx;
428
429 for (; dy <= 1; dy++)
430 for (dx = -1; dx <= 1; dx++)
431 if ((!dy && !dx) || (dy == bady && dx == badx) ||
432 y+dy < 0 && x+dx < 0 && y+dy >= HEIGHT && x+dx >= WIDTH)
433 /* don't do 0,0 or bad coordinates */
434 continue;
435 else {
436 int j=y, i=x, d=grid[y+dy][x+dx];
437
438 if (!d) continue;
439 do { /* "walk" the path, checking */
440 j += dy;
441 i += dx;
442 if (j < 0 || i < 0 || j >= HEIGHT ||
443 i >= WIDTH || !grid[j][i]) break;
444 } while (--d);
445 if (!d) return 1; /* if "d" got to 0, *
446 * move was okay. */
447 }
448 return 0; /* no good moves were found */
449 }
450
showmoves(bool on,int * attribs)451 void showmoves(bool on, int *attribs)
452 /*
453 * showmoves() is nearly identical to othermove(), but it highlights possible
454 * moves instead. "on" tells showmoves() whether to add or remove moves.
455 */
456 {
457 int dy = -1, dx;
458
459 for (; dy <= 1; dy++) {
460 if (y+dy < 0 || y+dy >= HEIGHT) continue;
461 for (dx = -1; dx <= 1; dx++) {
462 int j=y, i=x, d=grid[y+dy][x+dx];
463
464 if (!d) continue;
465 do {
466 j += dy;
467 i += dx;
468 if (j < 0 || i < 0 || j >= HEIGHT
469 || i >= WIDTH || !grid[j][i]) break;
470 } while (--d);
471 if (!d) {
472 int j=y, i=x, d=grid[y+dy][x+dx];
473
474 /* The next section chooses inverse-video *
475 * or not, and then "walks" chosen valid *
476 * move, reprinting characters with new mode */
477
478 if (on)
479 standout();
480 do {
481 j += dy;
482 i += dx;
483 #ifdef A_COLOR
484 if (!on && has_colors()) {
485 int newval = grid[j][i];
486 attron(attribs[newval - 1]);
487 mvaddch(j, i, newval + '0');
488 attroff(attribs[newval - 1]);
489 }
490 else
491 #endif
492 mvaddch(j, i, grid[j][i] + '0');
493 } while (--d);
494 if (on) standend();
495 }
496 }
497 }
498 }
499
doputc(int c)500 static int doputc(int c)
501 /* doputc() simply prints out a character to stdout, used by tputs() */
502 {
503 return(fputc(c, stdout));
504 }
505
506 /* hack to ignore GCC Unused Result */
507 #define IGNORE(r) do{if(r){}}while(0)
508
topscores(int newscore)509 static void topscores(int newscore)
510 /*
511 * topscores() processes its argument with the high score file, makes any
512 * updates to the file, and outputs the list to the screen. If "newscore"
513 * is false, the score file is printed to the screen (i.e. "greed -s")
514 */
515 {
516 int fd, count = 1;
517 static char termbuf[BUFSIZ];
518 char *tptr = (char *) malloc(16), *boldon, *boldoff;
519 struct score *toplist = (struct score *) malloc(SCOREFILESIZE);
520 struct score *ptrtmp, *eof = &toplist[MAXSCORES], *new = NULL;
521 extern char *getenv(), *tgetstr();
522 struct passwd *whoami;
523 void lockit(bool);
524
525 (void) signal(SIGINT, SIG_IGN); /* Catch all signals, so high */
526 (void) signal(SIGQUIT, SIG_IGN); /* score file doesn't get */
527 (void) signal(SIGTERM, SIG_IGN); /* messed up with a kill. */
528 (void) signal(SIGHUP, SIG_IGN);
529
530 whoami = getpwuid(getuid());
531
532 /* following open() creates the file if it doesn't exist
533 * already, using secure mode
534 */
535 if ((fd = open(SCOREFILE, O_RDWR|O_CREAT, 0600)) == -1) {
536 IGNORE(chdir(whoami->pw_dir));
537 if ((fd = open(LOCALSCOREFILE, O_RDWR|O_CREAT, 0600)) == -1) {
538 fprintf(stderr, "%s: ~/%s: Cannot open.\n", cmdname, LOCALSCOREFILE);
539 exit(1);
540 }
541 }
542
543 lockit(true); /* lock score file */
544 /* initialize scores to 0 */
545 for (ptrtmp=toplist; ptrtmp < eof; ptrtmp++)
546 ptrtmp->score = 0;
547 /* read whole score file in at once */
548 IGNORE(read(fd, toplist, SCOREFILESIZE));
549
550 if (newscore) { /* if possible high score */
551 for (ptrtmp=toplist; ptrtmp < eof; ptrtmp++)
552 /* find new location for score */
553 if (newscore > ptrtmp->score) break;
554 if (ptrtmp < eof) { /* if it's a new high score */
555 new = ptrtmp; /* put "new" at new location */
556 ptrtmp = eof-1; /* start at end of list */
557 while (ptrtmp > new) { /* shift list one down */
558 *ptrtmp = *(ptrtmp-1);
559 ptrtmp--;
560 }
561
562 new->score = newscore; /* fill "new" with the info */
563 new->time = time(NULL); /* include a timestamp */
564 strncpy(new->user, whoami->pw_name, USERNAMELEN);
565 (void) lseek(fd, 0, 0); /* seek back to top of file */
566 IGNORE(write(fd, toplist, SCOREFILESIZE)); /* write it all out */
567 }
568 }
569
570 close(fd);
571 lockit(false); /* unlock score file */
572
573 if (!toplist->score)
574 puts("No high scores."); /* perhaps "greed -s" was run before *
575 * any greed had been played? */
576 if (new && tgetent(termbuf, getenv("TERM")) > 0) {
577 /* grab escape sequences for standout */
578 boldon = tgetstr("so", &tptr);
579 boldoff = tgetstr("se", &tptr);
580 /* if only got one of the codes, use neither */
581 if (boldon==NULL || boldoff==NULL)
582 boldon = boldoff = NULL;
583 }
584
585 /* print out list to screen, highlighting new score, if any */
586 for (ptrtmp=toplist; ptrtmp < eof && ptrtmp->score; ptrtmp++, count++) {
587 struct tm when;
588 char timestr[27];
589 if (ptrtmp == new && boldon)
590 tputs(boldon, 1, doputc);
591 (void)localtime_r(&ptrtmp->time, &when);
592 (void)strftime(timestr, sizeof(timestr), "%Y-%m-%dT%H:%M:%S", &when);
593 printf("%-5d %s %6d %5.2f%% %s\n",
594 count,
595 timestr,
596 ptrtmp->score,
597 (float) (ptrtmp->score * 100) / (HEIGHT * WIDTH),
598 ptrtmp->user);
599 if (ptrtmp == new && boldoff) tputs(boldoff, 1, doputc);
600 }
601 }
602
603
lockit(bool on)604 void lockit(bool on)
605 /*
606 * lockit() creates a file with mode 0 to serve as a lock file. The creat()
607 * call will fail if the file exists already, since it was made with mode 0.
608 * lockit() will wait approx. 15 seconds for the lock file, and then
609 * override it (shouldn't happen, but might). "on" says whether to turn
610 * locking on or not.
611 */
612 {
613 int fd, x = 1;
614
615 if (on) {
616 while ((fd = open(LOCKPATH, O_RDWR | O_CREAT | O_EXCL, 0)) < 0) {
617 printf("Waiting for scorefile access... %d/15\n", x);
618 if (x++ >= 15) {
619 puts("Overriding stale lock...");
620 if (unlink(LOCKPATH) == -1) {
621 fprintf(stderr,
622 "%s: %s: Can't unlink lock.\n",
623 cmdname, LOCKPATH);
624 exit(1);
625 }
626 }
627 sleep(1);
628 }
629 close(fd);
630 } else unlink(LOCKPATH);
631 }
632
633 #define msg(row, msg) mvwaddstr(helpwin, row, 2, msg);
634
help(void)635 void help(void)
636 /*
637 * help() simply creates a new window over stdscr, and writes the help info
638 * inside it. Uses macro msg() to save space.
639 */
640 {
641 if (!helpwin) {
642 helpwin = newwin(18, 65, 1, 7);
643 #ifdef ACS_URCORNER
644 box(helpwin, ACS_VLINE, ACS_HLINE); /* print box around info */
645 (void) waddch(helpwin, ACS_ULCORNER);
646 mvwaddch(helpwin, 0, 64, ACS_URCORNER);
647 mvwaddch(helpwin, 17, 0, ACS_LLCORNER);
648 mvwaddch(helpwin, 17, 64, ACS_LRCORNER);
649 #else
650 box(helpwin, '|', '-'); /* print box around info */
651 /* put '+' at corners, looks better */
652 (void) waddch(helpwin, '+'); mvwaddch(helpwin, 0, 64, '+');
653 mvwaddch(helpwin, 17, 0, '+'); mvwaddch(helpwin, 17, 64, '+');
654 #endif
655
656 mvwprintw(helpwin, 1, 2,
657 "Welcome to %s, by Matthew Day <mday@iconsys.uu.net>.",version);
658 msg(3, " The object of Greed is to erase as much of the screen as");
659 msg(4, " possible by moving around in a grid of numbers. To move,");
660 msg(5, " use the arrow keys, your number pad, or one of the letters");
661 mvwprintw(helpwin, 6, 2,
662 " 'hjklyubn'. Your location is signified by the '%c' symbol.", ME);
663 msg(7, " When you move in a direction, you erase N number of grid");
664 msg(8, " squares in that direction, N being the first number in that");
665 msg(9, " direction. Your score reflects the total number of squares");
666 msg(10," eaten. Greed will not let you make a move that would have");
667 msg(11," placed you off the grid or over a previously eaten square");
668 msg(12," unless no valid moves exist, in which case your game ends.");
669 msg(13," Other Greed commands are 'Ctrl-L' to redraw the screen,");
670 msg(14," 'p' to toggle the highlighting of the possible moves, and");
671 msg(15," 'q' to quit. Command line options to Greed are '-s' to");
672 msg(16," output the high score file.");
673
674 (void) wmove(helpwin, 17, 64);
675 wrefresh(helpwin);
676 } else {
677 touchwin(helpwin);
678 wrefresh(helpwin);
679 }
680 (void) wgetch(helpwin);
681 touchwin(stdscr);
682 refresh();
683 }
684
685 /* the end */
686