view rantaiwarna.c @ 0:a9a7ad180c3b version-1

Initial revision
author Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
date Sat, 15 Mar 2014 18:41:03 +0100
parents
children 4f6bf50dbc4a
line wrap: on
line source

/*
 * Copyright (C) 2014 Guido Berhoerster <guido+rantaiwarna@berhoerster.name>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#define	_XOPEN_SOURCE	600

#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>
#include <math.h>
#include <time.h>
#include <sys/time.h>
#include <unistd.h>
#include <fcntl.h>
#include <locale.h>
#include <curses.h>
#include <term.h>

#include "rantaiwarna.h"
#include "compat.h"
#include "util.h"
#include "board.h"
#include "highscore.h"

#define	EXIT_USAGE	2
#define	HEIGHT_MAX	INT16_MAX
#define	HEIGHT_MIN	5
#define	WIDTH_MAX	INT16_MAX
#define	WIDTH_MIN	5
#define	DEFAULT_WIDTH	20
#define	DEFAULT_HEIGHT	10
#define	COLORS_MAX	6
#define	DEFAULT_COLORS	4
#define	ELEMENT_WIDTH	2
#define	SCORE_WIN_WIDTH	6
#define	HIGHSCORE_FMT	"%2d) %s %6" PRId32
#define	TIMESTR_MAX_SIZE 64
#define	MESSAGE_MAX_SIZE 64

enum {
	PIPE_R_FD = 0,
	PIPE_W_FD
};

enum {
	GAME_SCREEN,
	HIGHSCORE_SCREEN,
	HELP_SCREEN
};

struct rantaiwarna_ctx {
	struct board_ctx *board;
	struct highscore_entry *highscore;
	WINDOW		*game_header_win;
	WINDOW		*message_win;
	WINDOW		*score_win;
	WINDOW		*board_container_win;
	WINDOW		*board_win;
	WINDOW		*highscore_pad;
	WINDOW		*highscore_header_win;
	WINDOW		*help_pad;
	WINDOW		*help_header_win;
	WINDOW		*input_win;
	char		message[MESSAGE_MAX_SIZE];
	char		dummy_timestr[TIMESTR_MAX_SIZE];
	const chtype	*elements_chs;
	int		board_cursor_y;
	int		board_cursor_x;
	int		highscore_pos_y;
	int		highscore_pos_x;
	int		help_pos_y;
	int		help_pos_x;
	int		active_screen;
	bool		use_colors;
	bool		too_small;
};

char		*progname;
bool		curses_init = FALSE;
static int	signal_pipe_fd[2] = { -1, -1 };
static const chtype elements_chs[COLORS_MAX] = { ' ', '@', '#', '$', 'X', 'O' };
static const char *help_strs[] = {
	"rantaiwarna is a tile-matching puzzle game which is also known under "
	"the names ",
	"\"Chain Shot!\" or \"SameGame\". It is played on a rectangular board "
	"which is ",
	"initially filled with elements of several different colors. Two or "
	"more ",
	"adjacent elements of the same color may be eliminated, the score "
	"resulting from ",
	"the elimination of elements depends on the number of elements "
	"eliminated at ",
	"once. The goal of the game is to eliminate as many elements as "
	"possible until ",
	"there are no more adjacent elements of the same color left or the "
	"board is ",
	"completely cleared. Vertical gaps resulting from the elimination of "
	"elements ",
	"are filled by sliding down elements from above the gap, column gaps "
	"are filled ",
	"by sliding columns on the right side of the column gap to the left.",
	"",
	"Game Screen Commands",
	"",
	"    Arrow up, right, down, left",
	"        move the cursor around",
	"",
	"    h, j, k, l",
	"        same as arrow keys",
	"",
	"    Space",
	"        eliminate elements under the cursor",
	"",
	"    Enter",
	"        same as Space",
	"",
	"    Left mouse button",
	"        same as Space",
	"",
	"    Ctrl+l",
	"        refresh the screen",
	"",
	"    H",
	"        switch to the help screen",
	"",
	"    i",
	"        switch to the highscore screen",
	"",
	"    n",
	"        start a new game",
	"",
	"    q",
	"        quit the game",
	"",
	"Highscore Screen Commands",
	"",
	"    Arrow up, right, down, left",
	"        scroll on line up or down or one character to the left or"
	"right",
	"",
	"    h, j, k, l",
	"        same as arrow keys",
	"",
	"    Space",
	"        scroll down one line",
	"",
	"    Enter",
	"        same as Space",
	"",
	"    Page up, Page down",
	"        scroll up or down one screenful",
	"",
	"    b, f",
	"        same as Page up or Page down",
	"",
	"    Ctrl+b, Ctrl+f",
	"        same as Page up or Page down",
	"",
	"    Home, End",
	"        go to the first or last line",
	"",
	"    g, G,",
	"        same as Home or End",
	"",
	"    Ctrl+l",
	"        refresh the screen",
	"",
	"    a",
	"        switch to the game screen",
	"",
	"    H",
	"        switch to the help screen",
	"",
	"    q",
	"        quit the game",
	"",
	"Help Screen Commands",
	"",
	"    Arrow up, right, down, left",
	"        scroll on line up or down or one character to the left or "
	"right",
	"",
	"    h, j, k, l",
	"        same as arrow keys",
	"",
	"    Space",
	"        scroll down one line",
	"",
	"    Enter",
	"        same as Space",
	"",
	"    Page up, Page down",
	"        scroll up or down one screenful",
	"",
	"    b, f",
	"        same as Page up or Page down",
	"",
	"    Ctrl+b, Ctrl+f",
	"        same as Page up or Page down",
	"",
	"    Home, End",
	"        go to the first or last line",
	"",
	"    g, G,",
	"        same as Home or End",
	"",
	"    Ctrl+l",
	"        refresh the screen",
	"",
	"    a",
	"        switch to the game screen",
	"",
	"    i",
	"        switch to the highscore screen",
	"",
	"    q",
	"        quit the game",
	"",
	NULL
};

static void
on_signal(int signo)
{
	int		old_errno = errno;
	ssize_t		n;
	sigset_t	sigset;

	/* try to read unread signals from the pipe and add the new one to it */
	n = read(signal_pipe_fd[PIPE_R_FD], &sigset, sizeof (sigset));
	if (n == -1 || (size_t)n < sizeof (sigset)) {
		sigemptyset(&sigset);
	}
	sigaddset(&sigset, signo);
	write(signal_pipe_fd[PIPE_W_FD], &sigset, sizeof (sigset));

	errno = old_errno;
}

static int
rantaiwarna_doupdate(struct rantaiwarna_ctx *ctx)
{
	/*
	 * ensure the physical cursor is a sensible place in case it could not
	 * be made invisible
	 */
	if ((ctx->active_screen == GAME_SCREEN) && (ctx->board_win != NULL)) {
		wmove(ctx->board_win, ctx->board_cursor_y,
		    ctx->board_cursor_x * ELEMENT_WIDTH);
		wnoutrefresh(ctx->board_win);
	}
	return (doupdate());
}

static void
board_win_render_cursor(struct rantaiwarna_ctx *ctx, bool visible)
{
	/* highlighting color pairs are base color + COLORS_MAX */
	short	color = ctx->board->elements[ctx->board_cursor_y *
	    ctx->board->width + ctx->board_cursor_x] +
	    (visible ? COLORS_MAX : 0);
	int	i;
	chtype	ch;

	for (i = 0; i < ELEMENT_WIDTH; i++) {
		ch = mvwinch(ctx->board_win, ctx->board_cursor_y,
		    ctx->board_cursor_x * ELEMENT_WIDTH + i);
		if (visible) {
			/* use reverse and underline for highlighting */
			ch = (ch & ~A_COLOR) | A_REVERSE | A_UNDERLINE;
		} else {
			ch &= ~(A_COLOR | A_REVERSE | A_UNDERLINE);
		}
		if (ctx->use_colors) {
			ch |= COLOR_PAIR(color);
		}
		mvwaddch(ctx->board_win, ctx->board_cursor_y,
		    ctx->board_cursor_x * ELEMENT_WIDTH + i, ch);
	}
}

static void
board_win_update_cursor(struct rantaiwarna_ctx *ctx, bool visible)
{
	board_win_render_cursor(ctx, visible);
	touchwin(ctx->board_container_win);
	wnoutrefresh(ctx->board_win);
}

static int
board_win_move_cursor(struct rantaiwarna_ctx *ctx, int y, int x)
{
	if ((x < 0) || (x >= ctx->board->width) || (y < 0) ||
	    (y >= ctx->board->height)) {
		return (ERR);
	}

	board_win_update_cursor(ctx, FALSE);
	ctx->board_cursor_x = x;
	ctx->board_cursor_y = y;
	board_win_update_cursor(ctx, TRUE);

	return (OK);
}

static void
score_win_render(struct rantaiwarna_ctx *ctx)
{
	mvwprintw(ctx->score_win, 0, 0, "%*" PRId32, SCORE_WIN_WIDTH,
	    ctx->board->score);
}

static void
score_win_update(struct rantaiwarna_ctx *ctx)
{
	score_win_render(ctx);
	touchwin(ctx->game_header_win);
	wnoutrefresh(ctx->score_win);
}

static void
set_message(struct rantaiwarna_ctx *ctx, const char *message)
{
	snprintf(ctx->message, MESSAGE_MAX_SIZE, "%s", message);
}

static void
message_win_render(struct rantaiwarna_ctx *ctx)
{
	int	game_header_win_height;
	int	game_header_win_width;
	int	message_win_begy;
	int	message_win_begx;

	getmaxyx(ctx->game_header_win, game_header_win_height,
	    game_header_win_width);
	getbegyx(ctx->message_win, message_win_begy, message_win_begx);
	wclear(ctx->message_win);
	mvwaddstr(ctx->message_win, 0, (game_header_win_width -
	    (int)strlen(ctx->message)) / 2 - message_win_begx, ctx->message);
}

static void
message_win_update(struct rantaiwarna_ctx *ctx)
{
	message_win_render(ctx);
	touchwin(ctx->game_header_win);
	wnoutrefresh(ctx->message_win);
}

static void
board_win_render(struct rantaiwarna_ctx *ctx)
{
	chtype	ch;
	int	x;
	int	y;
	short	color;
	int	i;

	for (x = 0; x < ctx->board->width; x++) {
		for (y = 0; y < ctx->board->height; y++) {
			color = ctx->board->elements[y * ctx->board->width + x];
			ch = ctx->elements_chs[color];
			if (ctx->use_colors) {
				ch |= COLOR_PAIR(color);
			}
			for (i = 0; i < ELEMENT_WIDTH; i++) {
				mvwaddch(ctx->board_win, y,
				    x * ELEMENT_WIDTH + i, ch);
			}
		}
	}
}

static void
board_win_update(struct rantaiwarna_ctx *ctx)
{
	board_win_render(ctx);
	if ((ctx->board->status & GAME_OVER) == 0) {
		board_win_update_cursor(ctx, TRUE);
	}
	touchwin(ctx->board_container_win);
	wnoutrefresh(ctx->board_win);
}

static void
highscore_pad_update(struct rantaiwarna_ctx *ctx)
{
	struct highscore_entry *entry;
	int	i;
	int32_t	score;
	char	timestr[TIMESTR_MAX_SIZE];
	char	*timestrp;
	int	height;
	int	width;
	int	highscore_pad_height;
	int	highscore_pad_width;

	for (entry = ctx->highscore, i = 0; i < 10; i++) {
		if (entry != NULL) {
			score = entry->score;
			timestrp = timestr;
			if (strftime(timestr, sizeof (timestr), "%x",
			    &entry->time) == 0) {
				timestr[0] = '\0';
			}
			entry = entry->next;
		} else {
			score = 0;
			timestrp = ctx->dummy_timestr;
		}
		mvwprintw(ctx->highscore_pad, i, 0, HIGHSCORE_FMT, i + 1,
		    timestrp, score);
	}

	getmaxyx(stdscr, height, width);
	getmaxyx(ctx->highscore_pad, highscore_pad_height, highscore_pad_width);
	if (ctx->highscore_pos_y + height > highscore_pad_height) {
		ctx->highscore_pos_y = MAX(highscore_pad_height - height, 0);
	}
	if (ctx->highscore_pos_x + width > highscore_pad_width) {
		ctx->highscore_pos_x = MAX(highscore_pad_width - width, 0);
	}
}

static int
highscore_pad_refresh(struct rantaiwarna_ctx *ctx)
{
	int	height;
	int	width;
	int	highscore_pad_height;
	int	highscore_pad_width;

	getmaxyx(stdscr, height, width);
	getmaxyx(ctx->highscore_pad, highscore_pad_height, highscore_pad_width);

	return (pnoutrefresh(ctx->highscore_pad, ctx->highscore_pos_y,
	    ctx->highscore_pos_x, 1, 0, MIN(height - 1, highscore_pad_height -
	    1), MIN(width - 1, highscore_pad_width - 1)));
}

static int
help_pad_refresh(struct rantaiwarna_ctx *ctx)
{
	int	height;
	int	width;
	int	help_pad_height;
	int	help_pad_width;

	getmaxyx(stdscr, height, width);
	getmaxyx(ctx->help_pad, help_pad_height, help_pad_width);

	return (pnoutrefresh(ctx->help_pad, ctx->help_pos_y, ctx->help_pos_x,
	    1, 0, MIN(height - 1, help_pad_height - ctx->help_pos_y - 1),
	    MIN(width - 1, help_pad_width - ctx->help_pos_x - 1)));
}

static void
active_screen_switch(struct rantaiwarna_ctx *ctx)
{
	clear();
	wnoutrefresh(stdscr);
	switch (ctx->active_screen) {
	case GAME_SCREEN:
		touchwin(ctx->game_header_win);
		wnoutrefresh(ctx->game_header_win);
		touchwin(ctx->board_container_win);
		wnoutrefresh(ctx->board_container_win);
		touchwin(ctx->board_win);
		wnoutrefresh(ctx->board_win);
		ctx->input_win = ctx->board_win;
		break;
	case HIGHSCORE_SCREEN:
		touchwin(ctx->highscore_header_win);
		wnoutrefresh(ctx->highscore_header_win);
		highscore_pad_refresh(ctx);
		ctx->input_win = stdscr;
		break;
	case HELP_SCREEN:
		touchwin(ctx->help_header_win);
		wnoutrefresh(ctx->help_header_win);
		help_pad_refresh(ctx);
		ctx->input_win = stdscr;
		break;
	}
	flushinp();
}

static int
scrolled_win_handle_input(WINDOW *pad, int *pos_yp, int *pos_xp, int c)
{
	int	y = *pos_yp;
	int	x = *pos_xp;
	int	height;
	int	width;
	int	pad_height;
	int	pad_width;

	getmaxyx(stdscr, height, width);
	getmaxyx(pad, pad_height, pad_width);

	switch (c) {
	case 'k':
	case KEY_UP:
		/* scroll line up */
		y--;
		break;
	case 'l':
	case KEY_RIGHT:
		/* scroll right */
		x++;
		break;
	case 'j':
	case ' ':
	case '\n':
	case '\r':
	case KEY_ENTER:
	case KEY_DOWN:
		/* scroll line down */
		y++;
		break;
	case 'h':
	case KEY_LEFT:
		/* scroll left */
		x--;
		break;
	case 'b':
	case '\002':	/* ^B */
	case KEY_PPAGE:
		/* scroll up one screenful */
		y -= height;
		break;
	case 'f':
	case '\006':	/* ^F */
	case KEY_NPAGE:
		/* scroll down one screenful */
		y += height;
		break;
	case 'g':
	case KEY_HOME:
		/* go to the first line */
		y = 0;
		break;
	case 'G':
	case KEY_END:
		/* go to the line */
		y = MAX(pad_height - height, y);
		break;
	}

	if (y < 0) {
		y = 0;
	} else if (y + height > pad_height) {
		y = MAX(pad_height - height, 0);
	}
	if (x < 0) {
		x = 0;
	} else if (x + width > pad_width) {
		x = MAX(pad_width - width, 0);
	}

	if ((x != *pos_xp) || (y != *pos_yp)) {
		prefresh(pad, y, x, 1, 0, MIN(height - 1, pad_height - 1),
		    MIN(width - 1, pad_width - 1));
		*pos_yp = y;
		*pos_xp = x;
	}

	return (OK);
}

static int
highscore_handle_input(struct rantaiwarna_ctx *ctx, int c)
{
	switch (c) {
	case 'a':
		/* switch to game screen */
		ctx->active_screen = GAME_SCREEN;
		active_screen_switch(ctx);
		rantaiwarna_doupdate(ctx);
		return (OK);
	case 'H':
		/* switch to help screen */
		ctx->active_screen = HELP_SCREEN;
		active_screen_switch(ctx);
		rantaiwarna_doupdate(ctx);
		return (OK);
	default:
		/* delegate input handling to scrolled window handler */
		return (scrolled_win_handle_input(ctx->highscore_pad,
		    &ctx->highscore_pos_y, &ctx->highscore_pos_x, c));
	}
}

static int
help_handle_input(struct rantaiwarna_ctx *ctx, int c)
{
	switch (c) {
	case 'a':
		/* switch to game screen */
		ctx->active_screen = GAME_SCREEN;
		active_screen_switch(ctx);
		rantaiwarna_doupdate(ctx);
		return (OK);
	case 'i':
		/* switch to highscore screen */
		ctx->active_screen = HIGHSCORE_SCREEN;
		active_screen_switch(ctx);
		rantaiwarna_doupdate(ctx);
		return (OK);
	default:
		/* delegate input handling to scrolled window handler */
		return (scrolled_win_handle_input(ctx->help_pad,
		    &ctx->help_pos_y, &ctx->help_pos_x, c));
	}
}

static int
game_handle_input(struct rantaiwarna_ctx *ctx, int c)
{
	int	y = ctx->board_cursor_y;
	int	x = ctx->board_cursor_x;
	int	active_screen = ctx->active_screen;
	int	removed = 0;
#ifdef NCURSES_MOUSE_VERSION
	MEVENT	event;
	int	mx;
	int	my;
#endif /* NCURSES_MOUSE_VERSION */

	switch (c) {
	case 'k':
	case KEY_UP:
		/* move cursor up */
		y--;
		break;
	case 'l':
	case KEY_RIGHT:
		/* move cursor right */
		x++;
		break;
	case 'j':
	case KEY_DOWN:
		/* move cursor down */
		y++;
		break;
	case 'h':
	case KEY_LEFT:
		/* move cursor left */
		x--;
		break;
#ifdef NCURSES_MOUSE_VERSION
	case KEY_MOUSE:
		if (getmouse(&event) == OK) {
			mx = event.x;
			my = event.y;

			/* only handle mouse input inside the board window */
			if (wmouse_trafo(ctx->board_win, &my, &mx, FALSE)) {
				mx = (int)(floor(mx / ELEMENT_WIDTH));

				if (event.bstate & BUTTON1_RELEASED) {
					/* move cursor and remove element */
					y = my;
					x = mx;
					removed =
					    board_remove_elements(ctx->board,
					    y, x);
				} else if (event.bstate &
				    REPORT_MOUSE_POSITION) {
					/* move cursor to mouse position */
					y = my;
					x = mx;
				}
			}
		}
		break;
#endif /* NCURSES_MOUSE_VERSION */
	case KEY_ENTER:
	case '\n':
	case '\r':
	case ' ':
		/* remove current element */
		removed = board_remove_elements(ctx->board, y, x);
		break;
	case 'n':
		/* start a new game */
		ctx->board_cursor_x = ctx->board_cursor_y = 0;
		set_message(ctx, "rantaiwarna");
		if (board_generate(ctx->board) == OK) {
			score_win_update(ctx);
			message_win_update(ctx);
			board_win_update(ctx);
			rantaiwarna_doupdate(ctx);
			flash();
			return (OK);
		} else {
			return (ERR);
		}
	case 'H':
		/* switch to help screen */
		active_screen = HELP_SCREEN;
		break;
	case 'i':
		/* switch to highscore screen */
		active_screen = HIGHSCORE_SCREEN;
		break;
	}

	if (active_screen != ctx->active_screen) {
		ctx->active_screen = active_screen;
		active_screen_switch(ctx);
		rantaiwarna_doupdate(ctx);
	} else if (removed > 0) {
		score_win_update(ctx);
		board_win_update(ctx);
		if (ctx->board->status & GAME_OVER) {
			highscore_update(&ctx->highscore,
			    (int16_t)ctx->board->width,
			    (int16_t)ctx->board->height,
			    (int16_t)ctx->board->colors, ctx->board->score,
			    time(NULL));
			highscore_save(ctx->highscore, ctx->board->height,
			    ctx->board->width, ctx->board->colors);
			board_win_update_cursor(ctx, FALSE);
			ctx->board_cursor_y = ctx->board_cursor_x = 0;
			set_message(ctx, "GAME OVER");
			message_win_update(ctx);
			highscore_pad_update(ctx);
			flash();
		} else if ((y != ctx->board_cursor_y) ||
		    (x != ctx->board_cursor_x)) {
			board_win_move_cursor(ctx, y, x);
		}
		rantaiwarna_doupdate(ctx);
	} else if (((ctx->board->status & GAME_OVER) == 0) &&
	    ((y != ctx->board_cursor_y) || (x != ctx->board_cursor_x))) {
		if (board_win_move_cursor(ctx, y, x) == OK) {
			rantaiwarna_doupdate(ctx);
		}
	}

	return (OK);
}

static void
all_windows_delete(struct rantaiwarna_ctx *ctx)
{
	if (ctx == NULL) {
		return;
	}

	if (ctx->message_win != NULL) {
		delwin(ctx->message_win);
		ctx->message_win = NULL;
	}

	if (ctx->score_win != NULL) {
		delwin(ctx->score_win);
		ctx->score_win = NULL;
	}

	if (ctx->game_header_win != NULL) {
		delwin(ctx->game_header_win);
		ctx->game_header_win = NULL;
	}

	if (ctx->board_win != NULL) {
		delwin(ctx->board_win);
		ctx->board_win = NULL;
	}

	if (ctx->board_container_win != NULL) {
		delwin(ctx->board_container_win);
		ctx->board_container_win = NULL;
	}

	if (ctx->highscore_header_win != NULL) {
		delwin(ctx->highscore_header_win);
		ctx->highscore_header_win = NULL;
	}

	if (ctx->highscore_pad != NULL) {
		delwin(ctx->highscore_pad);
		ctx->highscore_pad = NULL;
	}

	if (ctx->help_header_win != NULL) {
		delwin(ctx->help_header_win);
		ctx->help_header_win = NULL;
	}

	if (ctx->help_pad != NULL) {
		delwin(ctx->help_pad);
		ctx->help_pad = NULL;
	}

}

static int
all_windows_create(struct rantaiwarna_ctx *ctx)
{
	int		width;
	int		height;
	char		score_label[] = "Score: ";
	int		score_label_len;
	int		header_min_width;
	char		highscore_title[] = "Highscore";
	int		highscore_entry_len;
	char		help_title[] = "Help";
	int		help_pad_height;
	int		help_pad_width;
	int		i;
	int		line_len;
	int		help_width;
	int		help_height;
	time_t		dummy_time = (time_t)0;

	getmaxyx(stdscr, height, width);

	/* enforce minimum width, both board and headers must be visble */
	score_label_len = (int)strlen(score_label);
	header_min_width = 20 + 1 + score_label_len + SCORE_WIN_WIDTH;
	header_min_width = MAX(header_min_width, (int)strlen(highscore_title));
	header_min_width = MAX(header_min_width, (int)strlen(help_title));
	if ((width < ctx->board->width * ELEMENT_WIDTH + 2) ||
	    (height < 1 + ctx->board->height + 2) ||
	    (width < header_min_width)) {
		ctx->too_small = TRUE;
		ctx->input_win = stdscr;
		mvaddstr(0, 0, "terminal size is too small");
		refresh();
		return (ERR);
	} else {
		ctx->too_small = FALSE;
	}

	/* set up game screen */
	ctx->game_header_win = newwin(1, width, 0, 0);
	if (ctx->game_header_win == NULL) {
		rantaiwarna_errx(1, "could not create window");
	}
	wbkgd(ctx->game_header_win, A_UNDERLINE | A_BOLD);
	mvwaddstr(ctx->game_header_win, 0, width - (score_label_len +
	    SCORE_WIN_WIDTH), score_label);

	ctx->score_win = derwin(ctx->game_header_win, 1, SCORE_WIN_WIDTH, 0,
	    width - SCORE_WIN_WIDTH);
	if (ctx->score_win == NULL) {
		rantaiwarna_errx(1, "could not create window");
	}
	score_win_render(ctx);

	ctx->message_win = derwin(ctx->game_header_win, 1, width - (1 +
	    score_label_len + SCORE_WIN_WIDTH), 0, 0);
	if (ctx->message_win == NULL) {
		rantaiwarna_errx(1, "could not create window");
	}
	message_win_render(ctx);

	ctx->board_container_win = newwin(ctx->board->height + 2,
	    ctx->board->width * ELEMENT_WIDTH + 2, (height - 1 -
	    (ctx->board->height + 2)) / 2, (width - (ctx->board->width *
	    ELEMENT_WIDTH + 2)) / 2);
	if (ctx->board_container_win == NULL) {
		rantaiwarna_errx(1, "could not create window");
	}
	box(ctx->board_container_win, 0, 0);

	ctx->board_win = derwin(ctx->board_container_win, ctx->board->height,
	    ctx->board->width * ELEMENT_WIDTH, 1, 1);
	if (ctx->board_win == NULL) {
		rantaiwarna_errx(1, "could not create window");
	}
	ctx->input_win = ctx->board_win;
	wbkgd(ctx->board_win, A_BOLD);
	wtimeout(ctx->board_win, 100);
	keypad(ctx->board_win, TRUE);
	board_win_render(ctx);

	board_win_render_cursor(ctx, TRUE);

	/* set up highscore screen */
	ctx->highscore_header_win = newwin(1, width, 0, 0);
	if (ctx->highscore_header_win == NULL) {
		rantaiwarna_errx(1, "could not create window");
	}
	wbkgd(ctx->highscore_header_win, A_UNDERLINE | A_BOLD);
	mvwaddstr(ctx->highscore_header_win, 0, 0, highscore_title);

	if (strftime(ctx->dummy_timestr, sizeof (ctx->dummy_timestr), "%x",
	    gmtime(&dummy_time)) == 0) {
		ctx->dummy_timestr[0] = '\0';
	}
	highscore_entry_len = snprintf(NULL, 0, HIGHSCORE_FMT, 1,
	    ctx->dummy_timestr, 0);
	if (highscore_entry_len < 0) {
		rantaiwarna_err(1, NULL);
	}
	ctx->highscore_pad = newpad(10, highscore_entry_len);
	if (ctx->highscore_pad == NULL) {
		rantaiwarna_errx(1, "could not create pad");
	}
	highscore_pad_update(ctx);

	/* set up help screen */
	ctx->help_header_win = newwin(1, width, 0, 0);
	if (ctx->help_header_win == NULL) {
		rantaiwarna_errx(1, "could not create window");
	}
	wbkgd(ctx->help_header_win, A_UNDERLINE | A_BOLD);
	mvwaddstr(ctx->help_header_win, 0, 0, help_title);

	for (i = 0, help_width = 1; help_strs[i] != NULL; i++) {
		line_len = (int)strlen(help_strs[i]);
		if (line_len > help_width) {
			help_width = line_len;
		}
	}
	help_height = i;
	ctx->help_pad = newpad(help_height, help_width);
	if (ctx->help_pad == NULL) {
		rantaiwarna_errx(1, "could not create pad");
	}

	for (i = 0; help_strs[i] != NULL; i++) {
		mvwaddstr(ctx->help_pad, i, 0, help_strs[i]);
	}
	getmaxyx(ctx->help_pad, help_pad_height, help_pad_width);
	if (ctx->help_pos_y + height - 1 > help_height) {
		ctx->help_pos_y = MAX(help_height - height - 1, 0);
	}
	if (ctx->help_pos_x + width > help_width) {
		ctx->help_pos_x = MAX(help_width - width, 0);
	}

	active_screen_switch(ctx);

	return (OK);
}

static void
handle_resize(struct rantaiwarna_ctx *ctx)
{
	all_windows_delete(ctx);
	clear();
	endwin();
	refresh();
	if (all_windows_create(ctx) == OK) {
		rantaiwarna_doupdate(ctx);
	}
}

static int
main_loop(struct rantaiwarna_ctx *ctx)
{
	sigset_t	sigset;
	sigset_t	old_sigset;
	ssize_t		n;
	int		saved_errno;
	int		c;

	while (TRUE) {
		/*
		 * deal with pending signals previously received in the signal
		 * handler, try to read a sigset from the pipe, avoid partial
		 * reads by blocking all signals during the read operation
		 */
		sigfillset(&sigset);
		sigprocmask(SIG_BLOCK, &sigset, &old_sigset);
		n = read(signal_pipe_fd[PIPE_R_FD], &sigset, sizeof (sigset));
		saved_errno = errno;
		sigprocmask(SIG_SETMASK, &old_sigset, NULL);
		if (n == -1) {
			if (saved_errno != EAGAIN) {
				/* unknown error */
				return (ERR);
			}
		} else if ((size_t)n != sizeof (sigset)) {
			/* short read, should not happen */
			return (ERR);
		} else {
			if ((sigismember(&sigset, SIGINT) == 1) ||
			    (sigismember(&sigset, SIGTERM) == 1) ||
			    (sigismember(&sigset, SIGQUIT) == 1) ||
			    (sigismember(&sigset, SIGHUP) == 1)) {
				return (ERR);
			}
#ifdef SIGWINCH
			if (sigismember(&sigset, SIGWINCH) == 1) {
				/* handle terminal resize */
				handle_resize(ctx);
			}
#endif /* SIGWINCH */
		}

		/* handle keyboard and mouse input */
		c = wgetch(ctx->input_win);
		/* only allow to quit if the terminal is too small */
		if (ctx->too_small) {
			if (c == 'q') {
				return (OK);
			} else {
				continue;
			}
		} else {
		/* handle global keys */
			switch (c) {
			case ERR:
				/* no input */
				continue;
			case 'q':
				/* quit */
				return (OK);
			case '\f':
				/* refresh */
				handle_resize(ctx);
				continue;
			default:
				/*
				 * delegate input handling to screen-specific
				 * handler
				 */
				switch (ctx->active_screen) {
				case GAME_SCREEN:
					if (game_handle_input(ctx, c) == ERR) {
						return (ERR);
					}
					break;
				case HIGHSCORE_SCREEN:
					if (highscore_handle_input(ctx, c) ==
					    ERR) {
						return (ERR);
					}
					break;
				case HELP_SCREEN:
					if (help_handle_input(ctx, c) == ERR) {
						return (ERR);
					}
					break;
				}
			}
		}
	}
}

int
main(int argc, char *argv[])
{
	int	status = EXIT_FAILURE;
	struct rantaiwarna_ctx *ctx = NULL;
	int	c;
	bool	errflag = FALSE;
	char	*endptr;
	long	colors = DEFAULT_COLORS;
	long	height = DEFAULT_HEIGHT;
	long	width = DEFAULT_WIDTH;
	int	flags;
	struct sigaction sigact;

	setlocale(LC_ALL, "");

	if ((progname = strrchr(argv[0], '/')) == NULL) {
		progname = argv[0];
	} else {
		progname++;
	}

	while (!errflag && (c = getopt(argc, argv, "c:h:w:")) != -1) {
		switch (c) {
		case 'c':
			errno = 0;
			colors = strtol(optarg, &endptr, 10);
			if ((errno != 0) || (*endptr != '\0') || (colors < 2) ||
			    (colors > COLORS_MAX - 1)) {
				errflag = TRUE;
			}
			break;
		case 'h':
			errno = 0;
			height = strtol(optarg, &endptr, 10);
			if ((errno != 0) || (*endptr != '\0') ||
			    (height < HEIGHT_MIN) || (height > HEIGHT_MAX)) {
				errflag = TRUE;
			}
			break;
		case 'w':
			errno = 0;
			width = strtol(optarg, &endptr, 10);
			if ((errno != 0) || (*endptr != '\0') ||
			    (width < WIDTH_MIN) || (width > WIDTH_MAX)) {
				errflag = TRUE;
			}
			break;
		default:
			errflag = TRUE;
		}
	}
	if (errflag) {
		fprintf(stderr, "usage: %s [-c colors] [-w width] "
		    "[-h height]\n", progname);
		status = EXIT_USAGE;
		goto out;
	}

	ctx = rantaiwarna_malloc(sizeof (struct rantaiwarna_ctx));
	ctx->board = board_create((int)height, (int)width, (short)colors);
	if (ctx->board->elements == NULL) {
		goto out;
	}
	ctx->active_screen = GAME_SCREEN;
	ctx->highscore = NULL;
	ctx->use_colors = FALSE;
	ctx->too_small = FALSE;
	ctx->game_header_win = NULL;
	ctx->message_win = NULL;
	ctx->score_win = NULL;
	ctx->board_container_win = NULL;
	ctx->board_win = NULL;
	ctx->highscore_pad = NULL;
	ctx->highscore_header_win = NULL;
	ctx->help_pad = NULL;
	ctx->help_header_win = NULL;
	ctx->input_win = stdscr;
	ctx->board_cursor_x = ctx->board_cursor_y = 0;
	ctx->highscore_pos_x = ctx->highscore_pos_y = 0;
	ctx->help_pos_x = ctx->help_pos_y = 0;
	ctx->message[0] = '\0';
	ctx->dummy_timestr[0] = '\0';
	ctx->elements_chs = elements_chs;

	highscore_load(&ctx->highscore, ctx->board->height, ctx->board->width,
	    ctx->board->colors);

	/* create pipe for delivering signals to a listener in the main loop */
	if (pipe(signal_pipe_fd) == -1) {
		goto out;
	}
	flags = fcntl(signal_pipe_fd[PIPE_R_FD], F_GETFL, 0);
	if ((flags == -1) || (fcntl(signal_pipe_fd[PIPE_R_FD], F_SETFL,
	    flags | O_NONBLOCK) == -1)) {
		goto out;
	}
	flags = fcntl(signal_pipe_fd[PIPE_W_FD], F_GETFL, 0);
	if ((flags == -1) || (fcntl(signal_pipe_fd[PIPE_W_FD], F_SETFL,
	    flags | O_NONBLOCK) == -1)) {
		goto out;
	}

	/* set up signal handler */
	sigact.sa_handler = on_signal;
	sigact.sa_flags = SA_RESTART;
	sigemptyset(&sigact.sa_mask);
	if ((sigaction(SIGINT, &sigact, NULL) < 0) ||
	    (sigaction(SIGTERM, &sigact, NULL) < 0) ||
	    (sigaction(SIGQUIT, &sigact, NULL) < 0) ||
	    (sigaction(SIGHUP, &sigact, NULL) < 0)) {
		goto out;
	}
#ifdef SIGWINCH
	if (sigaction(SIGWINCH, &sigact, NULL) < 0) {
		goto out;
	}
#endif /* SIGWINCH */

	/* initialize curses */
	initscr();
	curses_init = TRUE;
	cbreak();
	noecho();
	intrflush(stdscr, FALSE);
	nonl();
	curs_set(0);
	timeout(100);
	keypad(stdscr, TRUE);
#ifdef NCURSES_MOUSE_VERSION
	mousemask(REPORT_MOUSE_POSITION | ALL_MOUSE_EVENTS, NULL);
	mouseinterval(0);
#endif /* NCURSES_MOUSE_VERSION */
	if (has_colors()) {
		start_color();
		if ((COLORS >= 8) && (COLOR_PAIRS >= 12)) {
			ctx->use_colors = TRUE;
			/* colors 0 -- COLORS_MAX correspond to elements */
			init_pair(1, COLOR_RED, COLOR_RED);
			init_pair(2, COLOR_GREEN, COLOR_GREEN);
			init_pair(3, COLOR_YELLOW, COLOR_YELLOW);
			init_pair(4, COLOR_BLUE, COLOR_BLUE);
			init_pair(5, COLOR_MAGENTA, COLOR_MAGENTA);
			/*
			 * colors COLORS_MAX -- (COLORS_MAX + COLORS_MAX)
			 * are used for the cursor to highlight elements
			 */
			init_pair(6, COLOR_WHITE, COLOR_BLACK);
			init_pair(7, COLOR_WHITE, COLOR_RED);
			init_pair(8, COLOR_WHITE, COLOR_GREEN);
			init_pair(9, COLOR_WHITE, COLOR_YELLOW);
			init_pair(10, COLOR_WHITE, COLOR_BLUE);
			init_pair(11, COLOR_WHITE, COLOR_MAGENTA);
		}
	}

	set_message(ctx, "rantaiwarna");
	if (board_generate(ctx->board) == ERR) {
		goto out;
	}

	if (all_windows_create(ctx) == ERR) {
		goto out;
	}
	rantaiwarna_doupdate(ctx);

	if (main_loop(ctx) == OK) {
		status = EXIT_SUCCESS;
	}

out:
	restore_term();
	all_windows_delete(ctx);

	if (signal_pipe_fd[PIPE_R_FD] != -1) {
		close(signal_pipe_fd[PIPE_R_FD]);
	}
	if (signal_pipe_fd[PIPE_W_FD] != -1) {
		close(signal_pipe_fd[PIPE_W_FD]);
	}

	if (ctx != NULL) {
		board_free(ctx->board);
		highscore_free(ctx->highscore);
	}
	free(ctx);

	exit(status);
}