Mercurial > projects > rantaiwarna
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rantaiwarna.c Sat Mar 15 18:41:03 2014 +0100 @@ -0,0 +1,1258 @@ +/* + * 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); +}