/*
 * greed.c - Written by Matthew T. Day (mday@iconsys.uu.net), 09/06/89
 *
 * Now maintained by Eric S. Raymond <esr@snark.thyrsus.com>.  Matt
 * Day dropped out of sight and hasn't posted a new version or patch
 * in many years.
 *
 * 11/15/95 Fred C. Smith (Yes, the SAME Fred C. Smith who did the MS-DOS
 *          version), fix the 'p' option so when it removes the highlight
 *          from unused possible moves, it restores the previous color.
 *          -Some minor changes in the way it behaves at the end of a game,
 *          because I didn't like the way someone had changed it to work
 *          since I saw it a few years ago (personal preference).
 *          -Some style changes in the code, again personal preference.
 *          fredex@fcshome.stoneham.ma.us
 */

/*
 * When using a curses library with color capability, Greed will
 * detect color curses(3) if you have it and generate the board in
 * color, one color to each of the digit values. This will also enable
 * checking of an environment variable GREEDOPTS to override the
 * default color set, which will be parsed as a string of the form:
 *
 *	<c1><c2><c3><c4><c5><c6><c7><c8><c9>[:[p]]
 *
 * where <cn> is a character decribing the color for digit n.
 * The color letters are read as follows:
 *	b = blue,
 *	g = green,
 *	c = cyan,
 *	r = red,
 *	m = magenta,
 *	y = yellow,
 *	w = white.
 * In addition, capitalizing a letter turns on the A_BOLD attribute for that
 * letter.
 *
 * If the string ends with a trailing :, letters following are taken as game
 * options. At present, only 'p' (equivalent to an initial 'p' command) is
 * defined.
 *
 * SPDX-FileCopyrightText: (C) Eric S. Raymond <esr@thyrsus.com>
 * SPDX-License-Identifier: BSD-2-Clause
 */

static const char *version = "Greed v" RELEASE;

#include <ctype.h>
#include <curses.h>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <term.h>
#include <time.h>
#include <unistd.h>
#include <sys/ioctl.h>

#ifdef _DEBUG
#include <assert.h>
#endif

#define ME '@'

/*
 * The scorefile is fixed-length binary and consists of
 * structure images - very un-Unixy design!
 */
#define MAXSCORES 10
#define SCOREFILESIZE (MAXSCORES * sizeof(struct score))

/* rnd() returns a random number between 1 and x */
#define rnd(x) (int)((lrand48() % (x)) + 1)

#define LOCKPATH "/tmp/Greed.lock" /* lock path for high score file */

#define LOCALSCOREFILE ".greedscores"

#define USERNAMELEN 32 /* length of leading segment that we keep */

/*
 * changing stuff in this struct
 * makes old score files incompatible
 */
struct score {
	char user[USERNAMELEN + 1];
	time_t time;
	int height;
	int width;
	int maxstep;
	int score;
};

struct score_o {
	char user[USERNAMELEN + 1];
	time_t time;
	int score;
};

static int height = 22;
static int width = 79;
static int maxstep = 9;
static int status_row = 23;

static int *_grid = NULL;
static int y = 0, x = 0;
static bool allmoves = false, havebotmsg = false;
static int score = 0;
static char *cmdname = NULL;
static WINDOW *helpwin = NULL;

static void topscores(int);
static int tunnel(chtype, int *);
static int othermove(int, int);
static void showmoves(bool, int*);
static void lockit(bool);
static void help(void);

static void botmsg(char *msg, bool backcur) {
	/*
	 * botmsg() writes "msg" at the middle of the bottom line of the screen.
	 * Boolean "backcur" specifies whether to put cursor back on the grid or
	 * leave it on the bottom line (e.g. for questions).
	 */
	mvaddstr(status_row, 40, msg);
	clrtoeol();
	if (backcur) {
		move(y, x);
	}
	refresh();
	havebotmsg = true;
}

static void quit(int sig) {
	/*
	 * quit() is run when the user hits ^C or ^\, it queries the user if he
	 * really wanted to quit, and if so, checks the high score stuff (with
	 * the current score) and quits; otherwise, simply returns to the game.
	 */
	int ch;
	void (*osig)() = signal(SIGINT, SIG_IGN); /* save old signal */
	(void)signal(SIGQUIT, SIG_IGN);

	if (stdscr) {
		botmsg("Really quit? ", false);
		if ((ch = getch()) != 'y' && ch != 'Y') {
			move(y, x);
			(void)signal(SIGINT, osig); /* reset old signal */
			(void)signal(SIGQUIT, osig);
			refresh();
			return;
		}
		move(status_row, 0);
		refresh();
		endwin();
		puts("\n");
		topscores(score);
	}
	exit(0);
}

static void out(int onsig) {
	/*
	 * out() is run when the signal SIGTERM is sent, it corrects the
	 * terminal state (if necessary) and exits.
	 */
	if (stdscr) {
		endwin();
	}
	exit(0);
}

static void usage(void) {
	/* usage() prints out the proper command line usage for Greed and exits.
	 */
	fprintf(stderr, "%s\nUsage: %s [-s] [-p] [-f] [-w{width}] [-h{height}] [-m{maxstep}] [-r{seed}]\n", version, cmdname);
	exit(1);
}

static float score_perc(int score, int height, int width) {
	if (score <= 0) return 0.0;
	if (score >= (height * width - 1)) return 100.0;
	return ((float) score / (height * width - 1)) * 100.0;
}

static float score_adj(int score, int height, int width, int maxstep) {
	/* TODO: score should be adjusted basing on size.
	 *       different sizes imply different diffculties.
	 */
	if (maxstep <= 0) return 0;
	float score_p = score_perc(score, height, width);
	return (maxstep >= 9) ? score_p
			      : (score_p * (float)maxstep / 9.0);
}

static void showscore(void) {
	/*
	 * showscore() prints the score and the percentage of the screen eaten
	 * at the beginning of the bottom line of the screen, moves the
	 * cursor back on the grid, and refreshes the screen.
	 */
	mvprintw(status_row, 7, "%d  %.2f%%", score,
		 score_perc(score, height, width));
	move(y, x);
	refresh();
}

static void grid_free(void) {
	if (_grid) {
		free(_grid);
		_grid = NULL;
	}
}

static void grid_alloc(void) {
	grid_free();
	_grid = (int*) calloc(height * width, sizeof(int));
	if (!_grid) {
		perror("grid_alloc");
		exit(255);
	}
}

static int *grid_ptr(int y, int x) {
#ifdef _DEBUG
	assert(y >= 0 && y < height);
	assert(x >= 0 && x < width);
#endif
	return _grid + (y*width + x);
}

#if 0 && defined(_DEBUG)
static int *_X_grid_ptr(int line, int y, int x) {
	if (y < 0 || y >= height
		|| x < 0 || x >= width) {
		fprintf(stderr, "line=%d y=%d x=%d\n", line, y, x);
	}
	return grid_ptr(y, x);
}
#define grid_ptr(y, x) _X_grid_ptr(__LINE__, y, x)
#endif

int main(int argc, char **argv) {
	int val = 1;
	int attribs[10];
	char *colors;
	long seed = 0;
	bool seed_set = false;
	int i;

	cmdname = argv[0]; /* save the command name */

	for (int argi = 1; argi < argc; ++argi) {
		const char *const arg = argv[argi];
		const char opt = (arg[0] == '-') ? arg[1] : '\0';
		switch (opt) {
		case 'w': case 'h': case 'm': {
			const char *optarg = NULL;
			const char *cp;
			if (arg[2] == '\0') {
				if (++argi >= argc) {
					usage();
					return 1;
				}
				optarg = argv[argi];
			}
			else {
				optarg = arg + 2;
			}
			cp = optarg;
			while (isdigit((int)*cp)) ++cp;
			int optval = (cp > optarg && *cp == '\0') ? atoi(optarg) : -1;
			switch (opt) {
			case 'w': if (optval > 1) width = optval; break;
			case 'h': if (optval > 1) height = optval; break;
			case 'm': if (optval > 0 && optval < 9) maxstep = optval; break;
			}
		} break;
		case 'f': {
			struct winsize wnsz;
			if (ioctl(open("/dev/tty", O_RDONLY), TIOCGWINSZ, &wnsz) == 0) {
				if (wnsz.ws_col > 1) width = wnsz.ws_col - 1;
				if (wnsz.ws_row > 3) height = wnsz.ws_row - 3;
			}
		} break;
		case 'r': {
			const char *optarg = NULL;
			if (arg[2] == '\0') {
				if (++argi >= argc) {
					usage();
					return 1;
				}
				optarg = argv[argi];
			} else {
				optarg = arg + 2;
			}
			errno = 0;
			char *end = NULL;
			long optval = strtol(optarg, &end, 10);
			if (errno == 0 && end > optarg && *end == '\0') {
				seed = optval;
				seed_set = true;
			}
		} break;
		case 'p':
			allmoves = true;
			break;
		case 's':
			topscores(0);
			return 0;
		default:
			usage();
			return 1;
		}
	}

	if ((height+1)/2 < maxstep) maxstep = (height+1)/2;
	if ((width+1)/2 < maxstep) maxstep = (width+1)/2;

	status_row = height + 1;

	(void)signal(SIGINT, quit); /* catch off the signals */
	(void)signal(SIGQUIT, quit);
	(void)signal(SIGTERM, out);

	for (i = 0; i < 10; i++) {
		attribs[i] = 0;
	}
	attribs[0] = A_BOLD;

	initscr(); /* set up the terminal modes */
#ifdef KEY_MIN
	keypad(stdscr, true);
#endif /* KEY_MIN */
	cbreak();
	noecho();

	if (seed_set) {
		srand48(seed);
	} else {
		srand48(time(0) ^ getpid() << 16); /* initialize the random seed *
		                                    * with a unique number       */
	}

	if (has_colors()) {
		start_color();
		init_pair(0, COLOR_WHITE, COLOR_BLACK);
		init_pair(1, COLOR_YELLOW, COLOR_BLACK);
		init_pair(2, COLOR_RED, COLOR_BLACK);
		init_pair(3, COLOR_GREEN, COLOR_BLACK);
		init_pair(4, COLOR_CYAN, COLOR_BLACK);
		init_pair(5, COLOR_MAGENTA, COLOR_BLACK);

		attribs[0] = COLOR_PAIR(0) | A_BOLD;
		attribs[1] = COLOR_PAIR(1);
		attribs[2] = COLOR_PAIR(2);
		attribs[3] = COLOR_PAIR(3);
		attribs[4] = COLOR_PAIR(4);
		attribs[5] = COLOR_PAIR(5);
		attribs[6] = COLOR_PAIR(1) | A_BOLD;
		attribs[7] = COLOR_PAIR(2) | A_BOLD;
		attribs[8] = COLOR_PAIR(3) | A_BOLD;
		attribs[9] = COLOR_PAIR(4) | A_BOLD;

		if ((colors = getenv("GREEDOPTS")) != (char *)NULL) {
			static char *cnames = " rgybmcwRGYBMCW";
			char *cp;

			int idx;

			for (cp = colors, idx = 1;
			     *cp && *cp != ':' && idx <= 9; cp++, idx++) {
				if (strchr(cnames, *cp) != (char *)NULL) {
					if (*cp != ' ') {
						init_pair(idx,
						          strchr(cnames,
						                 tolower((unsigned char)*cp)) -
						              cnames,
						          COLOR_BLACK);
						attribs[idx] = COLOR_PAIR(idx);
						if (isupper((int)*cp)) {
							attribs[idx] |= A_BOLD;
						}
					}
				}
			}
			if (*cp == ':') {
				while (*++cp) {
					if (*cp == 'p') {
						allmoves = true;
					}
				}
			}
		}
	}

	grid_alloc();

	for (y = 0; y < height; y++) {        /* fill the grid array and */
		for (x = 0; x < width; x++) { /* print numbers out */
			if (has_colors()) {
				int newval = rnd(maxstep);

				attron(attribs[newval]);
				mvaddch(y, x, (*grid_ptr(y, x) = newval) + '0');
				attroff(attribs[newval]);
			} else {
				mvaddch(y, x, (*grid_ptr(y, x) = rnd(maxstep)) + '0');
			}
		}
	}

	mvaddstr(status_row, 0, "Score: "); /* initialize bottom line */
	mvprintw(status_row, 40, "%s - Hit '?' for help.", version);
	y = rnd(height) - 1;
	x = rnd(width) - 1; /* random initial location */
	attron(attribs[0]);
	mvaddch(y, x, ME);
	attroff(attribs[0]);
	*grid_ptr(y, x) = 0; /* eat initial square */

	if (allmoves) {
		showmoves(true, attribs);
	}
	showscore();

	/* main loop, gives tunnel() a user command */
	while ((val = tunnel(getch(), attribs)) > 0) {
		continue;
	}

	if (!val) {                             /* if didn't quit by 'q' cmd */
		botmsg("Hit any key..", false); /* then let user examine     */
		getch();                        /* final screen              */
	}

	move(status_row, 0);
	refresh();
	endwin();
	puts("\n"); /* writes two newlines */
	topscores(score);

	grid_free();

	exit(0);
}

static int tunnel(chtype cmd, int *attribs) {
	/*
	 * tunnel() does the main game work.  Returns 1 if everything's okay, 0
	 * if user "died", and -1 if user specified and confirmed 'q' (fast
	 * quit).
	 */
	int dy = 0, dx = 0, distance = 0;

	switch (cmd) { /* process user command */
	case 'h':
	case 'H':
	case '4':
#ifdef KEY_LEFT
	case KEY_LEFT:
#endif /* KEY_LEFT */
		dy = 0;
		dx = -1;
		break;
	case 'j':
	case 'J':
	case '2':
#ifdef KEY_DOWN
	case KEY_DOWN:
#endif /* KEY_DOWN */
		dy = 1;
		dx = 0;
		break;
	case 'k':
	case 'K':
	case '8':
#ifdef KEY_UP
	case KEY_UP:
#endif /* KEY_UP */
		dy = -1;
		dx = 0;
		break;
	case 'l':
	case 'L':
	case '6':
#ifdef KEY_RIGHT
	case KEY_RIGHT:
#endif /* KEY_RIGHT */
		dy = 0;
		dx = 1;
		break;
	case 'b':
	case 'B':
	case '1':
		dy = 1;
		dx = -1;
		break;
	case 'n':
	case 'N':
	case '3':
		dy = dx = 1;
		break;
	case 'y':
	case 'Y':
	case '7':
		dy = dx = -1;
		break;
	case 'u':
	case 'U':
	case '9':
		dy = -1;
		dx = 1;
		break;
	case 'p':
	case 'P':
		allmoves = !allmoves;
		showmoves(allmoves, attribs);
		move(y, x);
		refresh();
		return (1);
	case 'q':
	case 'Q':
		quit(0);
		return (1);
	case '?':
		help();
		return (1);
	case '\14':
	case '\22':               /* ^L or ^R (redraw) */
		wrefresh(curscr);
		break;
	default:
		wrefresh(curscr);
		return (1);
	}
	distance =
	    (y + dy >= 0 && x + dx >= 0 && y + dy < height && x + dx < width)
	        ? *grid_ptr(y + dy, x + dx)
	        : 0;

	{
		int j = y, i = x, d = distance;

		do { /* process move for validity */
			j += dy;
			i += dx;
			if (j >= 0 && i >= 0 && j < height && i < width &&
			    *grid_ptr(j, i)) {
				continue; /* if off the screen */
			} else if (!othermove(dy,
			                      dx)) { /* no other good move */
				j -= dy;
				i -= dx;
				mvaddch(y, x, ' ');
				while (y != j || x != i) {
					y += dy; /* so we manually */
					x += dx; /* print chosen path */
					score++;
					mvaddch(y, x, ' ');
				}
				mvaddch(y, x, '*'); /* with a '*' */
				showscore();        /* print final score */
				return (0);
			} else { /* otherwise prevent bad move */
				botmsg("Bad move.", true);
				return (1);
			}
		} while (--d);
	}

	/* remove possible moves */
	if (allmoves) {
		showmoves(false, attribs);
	}

	if (havebotmsg) { /* if old bottom msg exists */
		mvprintw(status_row, 40, "%s - Hit '?' for help.", version);
		havebotmsg = false;
	}

	mvaddch(y, x, ' '); /* erase old ME */
	do {                /* print good path */
		y += dy;
		x += dx;
		score++;
		*grid_ptr(y, x) = 0;
		mvaddch(y, x, ' ');
	} while (--distance);
	attron(attribs[0]);
	mvaddch(y, x, ME); /* put new ME */
	attroff(attribs[0]);
	if (allmoves) {
		showmoves(true, attribs); /* put new possible moves */
	}
	showscore(); /* does refresh() finally */
	return (1);
}

static int othermove(int bady, int badx) {
	/*
	 * othermove() checks area for an existing possible move.  bady and
	 * badx are direction variables that tell othermove() they are
	 * already no good, and to not process them.  I don't know if this
	 * is efficient, but it works!
	 */
	int dy = -1, dx;

	for (; dy <= 1; dy++) {
		for (dx = -1; dx <= 1; dx++) {
			if ((!dy && !dx) || (dy == bady && dx == badx) ||
			    y + dy < 0 || x + dx < 0 || y + dy >= height ||
			        x + dx >= width) {
				/* don't do 0,0 or bad coordinates */
				continue;
			} else {
				int j = y, i = x, d = *grid_ptr(y + dy, x + dx);

				if (!d) {
					continue;
				}
				do { /* "walk" the path, checking */
					j += dy;
					i += dx;
					if (j < 0 || i < 0 || j >= height ||
					    i >= width || !*grid_ptr(j, i)) {
						break;
					}
				} while (--d);
				if (!d) {
					return 1; /* if "d" got to 0, *
					           * move was okay.   */
				}
			}
		}
	}
	return 0; /* no good moves were found */
}

static void showmoves(bool on, int *attribs) {
	/*
	 * showmoves() is nearly identical to othermove(), but it highlights
	 * possible moves instead.  "on" tells showmoves() whether to add or
	 * remove moves.
	 */
	int dy = -1, dx;

	for (; dy <= 1; dy++) {
		if (y + dy < 0 || y + dy >= height) {
			continue;
		}
		for (dx = -1; dx <= 1; dx++) {
			if (x + dx < 0 || x + dx >= width) {
				continue;
			}
			int j = y, i = x, d = *grid_ptr(y + dy, x + dx);

			if (!d) {
				continue;
			}
			do {
				j += dy;
				i += dx;
				if (j < 0 || i < 0 || j >= height ||
				    i >= width || !*grid_ptr(j, i)) {
					break;
				}
			} while (--d);
			if (!d) {
				int j = y, i = x, d = *grid_ptr(y + dy, x + dx);

				/* The next section chooses inverse-video    *
				 * or not, and then "walks" chosen valid     *
				 * move, reprinting characters with new mode */

				if (on) {
					standout();
				}
				do {
					j += dy;
					i += dx;
					if (!on && has_colors()) {
						int newval = *grid_ptr(j, i);
						attron(attribs[newval]);
						mvaddch(j, i, newval + '0');
						attroff(attribs[newval]);
					} else {
						mvaddch(j, i, *grid_ptr(j, i) + '0');
					}
				} while (--d);
				if (on) {
					standend();
				}
			}
		}
	}
}

static int doputc(int c) {
	/* doputc() simply prints out a character to stdout, used by tputs() */
	return (fputc(c, stdout));
}

/* hack to ignore GCC Unused Result */
#define IGNORE(r)                                                              \
	do {                                                                   \
		if (r) {                                                       \
		}                                                              \
	} while (0)

static void topscores(int newscore) {
	/*
	 * topscores() processes its argument with the high score file, makes
	 * any updates to the file, and outputs the list to the screen.  If
	 * "newscore" <= 0, the score file is printed to the screen (i.e.
	 * "greed -s")
	 */
	int fd, count = 1;
	static char termbuf[BUFSIZ];
	char *tptr = (char *)malloc(2048), *boldon = NULL, *boldoff = NULL;
	struct score *toplist = (struct score *)malloc(SCOREFILESIZE);
	struct score *ptrtmp, *eof = &toplist[MAXSCORES], *new = NULL;
	struct passwd *whoami;
	void lockit(bool);
	const char *termname;

	if (!tptr || !toplist) {
		fprintf(stderr, "%s: out of memory.\n", cmdname);
		exit(1);
	}

	(void)signal(SIGINT, SIG_IGN);  /* Catch all signals, so high */
	(void)signal(SIGQUIT, SIG_IGN); /* score file doesn't get     */
	(void)signal(SIGTERM, SIG_IGN); /* messed up with a kill.     */
	(void)signal(SIGHUP, SIG_IGN);

	whoami = getpwuid(getuid());
	if (!whoami) {
		fprintf(stderr, "%s: no passwd entry for uid %ld.\n", cmdname,
		        (long)getuid());
		exit(1);
	}

	termname = getenv("TERM");
	if (!termname) {
		fprintf(stderr, "%s: TERM is not set.\n", cmdname);
		exit(1);
	}

	/* following open() creates the file if it doesn't exist
	 * already, using secure mode
	 */
	if ((fd = open(SCOREFILE, O_RDWR | O_CREAT, 0600)) == -1) {
		IGNORE(chdir(whoami->pw_dir));
		if ((fd = open(LOCALSCOREFILE, O_RDWR | O_CREAT, 0600)) == -1) {
			fprintf(stderr, "%s: ~/%s: Cannot open.\n", cmdname,
			        LOCALSCOREFILE);
			exit(1);
		}
	}

	lockit(true); /* lock score file */
	/* initialize scores to 0 */
	for (ptrtmp = toplist; ptrtmp < eof; ptrtmp++) {
		ptrtmp->score = 0;
	}
	/* read whole score file in at once */
	IGNORE(read(fd, toplist, SCOREFILESIZE));

	if (newscore > 0) { /* if possible high score */
		float newscore_p = score_adj(newscore, height, width, maxstep);
		for (ptrtmp = toplist; ptrtmp < eof; ptrtmp++) {
			/* find new location for score */
			if (newscore_p > score_adj(ptrtmp->score, ptrtmp->height,
						   ptrtmp->width, ptrtmp->maxstep)) {
				break;
			}
		}
		if (ptrtmp < eof) {            /* if it's a new high score */
			new = ptrtmp;          /* put "new" at new location */
			ptrtmp = eof - 1;      /* start at end of list */
			while (ptrtmp > new) { /* shift list one down */
				*ptrtmp = *(ptrtmp - 1);
				ptrtmp--;
			}

			new->score = newscore;  /* fill "new" with the info */
			new->time = time(NULL); /* include a timestamp */
			new->height = height;
			new->width = width;
			new->maxstep = maxstep;
			strncpy(new->user, whoami->pw_name, USERNAMELEN);
			new->user[USERNAMELEN] = '\0';
			(void)lseek(fd, 0, 0); /* seek back to top of file */
			IGNORE(write(fd, toplist,
			             SCOREFILESIZE)); /* write it all out */
		}
	}

	close(fd);
	lockit(false); /* unlock score file */

	if (!toplist->score) {
		puts("No high scores."); /* perhaps "greed -s" was run before *
		                          * any greed had been played? */
	}
	if (new &&tgetent(termbuf, termname) > 0) {
		/* grab escape sequences for standout */
		boldon = tgetstr("so", &tptr);
		boldoff = tgetstr("se", &tptr);
		/* if only got one of the codes, use neither */
		if (boldon == NULL || boldoff == NULL) {
			boldon = boldoff = NULL;
		}
	}

	/* print out list to screen, highlighting new score, if any */
	for (ptrtmp = toplist; ptrtmp < eof && ptrtmp->score;
	     ptrtmp++, count++) {
		struct tm when;
		char timestr[32];
		char sizestr[64];
		if (ptrtmp == new &&boldon) {
			tputs(boldon, 1, doputc);
		}
		(void)localtime_r(&ptrtmp->time, &when);
		(void)strftime(timestr, sizeof(timestr), "%Y-%m-%dT%H:%M:%S",
		               &when);
		if (ptrtmp->maxstep < 9)
			(void)snprintf(sizestr, sizeof(sizestr), "(%dx%d:%d)",
				       ptrtmp->width, ptrtmp->height, ptrtmp->maxstep);
		else
			(void)snprintf(sizestr, sizeof(sizestr), "(%dx%d)",
				       ptrtmp->width, ptrtmp->height);
		printf("%-5d %s %6d %-10s %6.2f%% %s\n",
		       count,
		       timestr,
		       ptrtmp->score,
		       sizestr,
		       score_perc(ptrtmp->score, ptrtmp->height, ptrtmp->width),
		       ptrtmp->user);
		if (ptrtmp == new &&boldoff) {
			tputs(boldoff, 1, doputc);
		}
	}

	free(toplist);
}

static void lockit(bool on) {
	/*
	 * lockit() creates a file with mode 0 to serve as a lock file.  The
	 * creat() call will fail if the file exists already, since it was made
	 * with mode 0. lockit() will wait approx. 15 seconds for the lock file,
	 * and then override it (shouldn't happen, but might).  "on" says
	 * whether to turn locking on or not.
	 */
	int fd, x = 1;

	if (on) {
		while ((fd = open(LOCKPATH, O_RDWR | O_CREAT | O_EXCL, 0)) <
		       0) {
			printf("Waiting for scorefile access... %d/15\n", x);
			if (x++ >= 15) {
				puts("Overriding stale lock...");
				if (unlink(LOCKPATH) == -1) {
					fprintf(stderr,
					        "%s: %s: Can't unlink lock.\n",
					        cmdname, LOCKPATH);
					exit(1);
				}
			}
			sleep(1);
		}
		close(fd);
	} else {
		unlink(LOCKPATH);
	}
}

#define msg(row, msg) mvwaddstr(helpwin, row, 2, msg);

static void help(void) {
	/*
	 * help() simply creates a new window over stdscr, and writes the help
	 * info inside it.  Uses macro msg() to save space.
	 */
	if (!helpwin) {
		helpwin = newwin(18, 65, 1, 7);
#ifdef ACS_URCORNER
		box(helpwin, ACS_VLINE, ACS_HLINE); /* print box around info */
		(void)waddch(helpwin, ACS_ULCORNER);
		mvwaddch(helpwin, 0, 64, ACS_URCORNER);
		mvwaddch(helpwin, 17, 0, ACS_LLCORNER);
		mvwaddch(helpwin, 17, 64, ACS_LRCORNER);
#else
		box(helpwin, '|', '-'); /* print box around info */
		/* put '+' at corners, looks better */
		(void)waddch(helpwin, '+');
		mvwaddch(helpwin, 0, 64, '+');
		mvwaddch(helpwin, 17, 0, '+');
		mvwaddch(helpwin, 17, 64, '+');
#endif

		mvwprintw(
		    helpwin, 1, 2,
		    "Welcome to %s, by Matthew Day <mday@iconsys.uu.net>.",
		    version);
		// clang-format off
		msg(3,  " The object of Greed is to erase as much of the screen as");
		msg(4,  " possible by moving around in a grid of numbers.  To move,");
		msg(5,  " use the arrow keys, your number pad, or one of the letters");
		mvwprintw(helpwin, 6, 2,
		        " 'hjklyubn'. Your location is signified by the '%c' symbol.", ME);
		msg(7,  " When you move in a direction, you erase N number of grid");
		msg(8,  " squares in that direction, N being the first number in that");
		msg(9,  " direction.  Your score reflects the total number of squares");
		msg(10, " eaten.  Greed will not let you make a move that would have");
		msg(11, " placed you off the grid or over a previously eaten square");
		msg(12, " unless no valid moves exist, in which case your game ends.");
		msg(13, " Other Greed commands are 'Ctrl-L' to redraw the screen,");
		msg(14, " 'p' to toggle the highlighting of the possible moves, and");
		msg(15, " 'q' to quit.  Command line options include '-s' (scores)");
		msg(16, " and '-r' to set the RNG seed.");
		// clang-format on

		(void)wmove(helpwin, 17, 64);
		wrefresh(helpwin);
	} else {
		touchwin(helpwin);
		wrefresh(helpwin);
	}
	(void)wgetch(helpwin);
	touchwin(stdscr);
	refresh();
}

/* the end */
