X-Git-Url: https://git.draconx.ca/gitweb/rrace.git/blobdiff_plain/7c0e8a953d0057ec04f5d77c713a34c1ab30a7d6..7a8e9a559a8d08937a0fde473fb992f18e74132f:/src/curses.c diff --git a/src/curses.c b/src/curses.c index bf97caf..8405e23 100644 --- a/src/curses.c +++ b/src/curses.c @@ -25,17 +25,18 @@ #include "help.h" #include "version.h" + +#include "cursesui.h" #include "cursesopt.h" -#include "game.h" + +enum { + GAME_YPOS = 1 // top row of game and goal areas. +}; static const char *progname = "rrace"; static const struct option lopts[] = { LOPTS_INITIALIZER, {0} }; -static struct app_state { - struct board board; - - WINDOW *tile_border, *tile_fill; -} state; +static struct app_state state; static void print_version(void) { @@ -71,11 +72,11 @@ static void print_help(void) printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT); } -static void -draw_tile(struct app_state *state, unsigned colour, unsigned x, unsigned y) +static void draw_tile(WINDOW **win, unsigned colour, unsigned selected, + unsigned x, unsigned y, unsigned start_column) { - int attr, ch; - int w, h; + WINDOW *border = win[PLAYWIN_TILEBORDER], *fill = win[PLAYWIN_TILEFILL]; + int w, h, attr, ch, bc = selected ? '#' : 0; assert(colour < TILE_MAX); attr = COLOR_PAIR(colour); @@ -86,85 +87,218 @@ draw_tile(struct app_state *state, unsigned colour, unsigned x, unsigned y) case TILE_YELLOW: ch = '~'; attr |= A_BOLD; break; case TILE_BLUE: ch = 'o'; attr |= A_BOLD; break; case TILE_WHITE: ch = '.'; attr |= A_BOLD; break; + + case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(RR_COLOUR_SHADOW); } - getmaxyx(state->tile_border, h, w); + getmaxyx(border, h, w); w = 2*(w+1)/2; - if (mvwin(state->tile_border, 2+h*y, 4+w*x) == ERR) + if (mvwin(border, 1+GAME_YPOS+h*y, start_column+w*x) == ERR) return; if (colour != TILE_EMPTY) { - wattrset(state->tile_border, attr); - box(state->tile_border, 0, 0); - - mvderwin(state->tile_fill, 1, 1); - wbkgdset(state->tile_fill, A_REVERSE|attr|ch); - werase(state->tile_fill); + mvderwin(fill, 1, 1); + wbkgdset(fill, A_REVERSE|attr|ch); + werase(fill); } else { - werase(state->tile_border); + werase(border); } - wnoutrefresh(state->tile_border); + if (bc || colour) { + wattrset(border, attr); + wborder(border, bc, bc, bc, bc, bc, bc, bc, bc); + } + + wnoutrefresh(border); } -static int curs_redraw_tile(struct app_state *state, unsigned x, unsigned y) +static int +redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column, + uint_fast32_t bit0, uint_fast32_t bit1, uint_fast32_t bit2, + unsigned selected) { uint_fast32_t pos = board_position(x, y); unsigned char tile = 0; - if (state->board.game[0] & pos) tile |= 1; - if (state->board.game[1] & pos) tile |= 2; - if (state->board.game[2] & pos) tile |= 4; + if (bit0 & pos) tile |= 1; + if (bit1 & pos) tile |= 2; + if (bit2 & pos) tile |= 4; assert(tile < TILE_MAX); - draw_tile(state, tile, x, y); + draw_tile(win, tile, selected, x, y, start_column); return tile; } +static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz) +{ + int w, h, tr = 0, rs = 0, br = 0, bs = 0, bl = 0; + WINDOW *area = win[PLAYWIN_AREA]; + + getmaxyx(stdscr, h, w); + + if (h <= GAME_YPOS+1) { + bl = ACS_ULCORNER, br = ACS_URCORNER; + } else if (h <= 3*sz+GAME_YPOS+1) { + bl = br = ACS_VLINE, bs = ' '; + } + + if (w <= x+1) { + tr = ACS_ULCORNER, br = ACS_LLCORNER; + } else if (w <= 6*sz+x+2) { + tr = ACS_HLINE, rs = ' '; + br = br ? ' ' : ACS_HLINE; + } + + wborder(area, 0, rs, 0, bs, 0, tr, bl, br); + wnoutrefresh(area); +} + static void curs_redraw_game(struct app_state *state, uint_fast32_t mask) { + uint_least32_t buf[3], *gp = state->board.game; int i; + if (mask == -1) + redraw_area_border(state->gamewin, 2, 5); + + if (state->view_goal_on_game) { + for (i = 0; i < 3; i++) { + buf[i] = state->board.goal[i]; + buf[i] = (gp[i] & ~GOAL_MASK) | (buf[i] << GOAL_SHIFT); + } + gp = buf; + } + for (i = 0; i < 25; i++) { if (mask & 1) { - curs_redraw_tile(state, i%5, i/5); + redraw_tile(state->gamewin, i%5, i/5, + 4, gp[0], gp[1], gp[2], + i == state->cursor); } mask >>= 1; } } -static void curs_alloc_tiles(struct app_state *state) +static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask) { - int w, h, tilesz; - WINDOW *tile; + uint_least16_t *gp = state->board.goal; + int i, x, y; - getmaxyx(stdscr, h, w); - tilesz = (h - 4) / 5; - if (tilesz < 3) - tilesz = 3; + if (!state->goalwin[PLAYWIN_AREA]) + return; + + getbegyx(state->goalwin[PLAYWIN_AREA], y, x); + if (mask == -1) + redraw_area_border(state->goalwin, x, 3); - h = -1; - if (state->tile_border) { - getmaxyx(state->tile_border, h, w); + for (i = 0; i < 9; i++) { + if (mask & 1) { + redraw_tile(state->goalwin, i%3, i/3, + x+2, gp[0], gp[1], gp[2], 0); + } + mask >>= 1; } +} - if (h == tilesz) { - /* Nothing to do. */ - return; +/* Thin wrapper around wresize which returns ERR if support is unavailable. */ +static int do_wresize(WINDOW *window, int lines, int columns) +{ +#if HAVE_CURSES_WRESIZE + return wresize(window, lines, columns); +#endif + return ERR; +} + +static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x) +{ + WINDOW *win = *orig; + + if (win) { + if (do_wresize(win, h, w) != ERR) { + mvwin(win, y, x); + return win; + } + + delwin(win); } - if (state->tile_border) { - delwin(state->tile_fill); - delwin(state->tile_border); + if (w > 0 && h > 0) + return *orig = subwin(stdscr, h, w, y, x); + return *orig = NULL; +} + +static void realloc_tiles(WINDOW **win, int h) +{ + WINDOW *border = win[PLAYWIN_TILEBORDER], *fill = win[PLAYWIN_TILEFILL]; + int w = 2*h - 1; + + if (fill && border) { + int old_w, old_h; + + if (do_wresize(fill, h-2, w-2) != ERR + && do_wresize(border, h, w) != ERR) + return; + + getmaxyx(border, old_h, old_w); + if (old_h == h) + return; + } + + if (fill) + delwin(fill); + if (border) + delwin(border); + + win[PLAYWIN_TILEBORDER] = border = newwin(h, w, 0, 0); + win[PLAYWIN_TILEFILL] = derwin(border, h-2, w-2, 1, 1); +} + +static void setup_mainwin(struct app_state *state) +{ + int w, h, gamesz, goalsz, scr_w, scr_h, split; + + getmaxyx(stdscr, scr_h, scr_w); + + /* First try to fit the game tiles based on window height. */ + gamesz = MAX(3, (scr_h - 4) / 5); + + /* Adjust downward until we can fit smallest possible goal area. */ + for (; split = 5+10*gamesz, gamesz > 3; gamesz--) { + if (split + 20 < scr_w) + break; } - state->tile_border = tile = newwin(tilesz, 2*tilesz-1, 0, 0); - state->tile_fill = derwin(tile, tilesz-2, 2*tilesz-3, 1, 1); + /* Pick a goal size that will fit in the remaining area */ + goalsz = MAX(3, (scr_w - split - 4) / 6); + if (goalsz >= gamesz) + goalsz = MAX(3, gamesz - 1); + + realloc_tiles(state->gamewin, gamesz); + realloc_tiles(state->goalwin, goalsz); + + /* Frame for game area */ + w = MIN(scr_w-2, 3+10*gamesz); + h = MIN(scr_h-GAME_YPOS, 2+5*gamesz); + realloc_area(&state->gamewin[PLAYWIN_AREA], h, w, GAME_YPOS, 2); + + /* Frame for goal area */ + w = MIN(scr_w-split, 3+6*goalsz); + h = MIN(scr_h-GAME_YPOS, 2+3*goalsz); + realloc_area(&state->goalwin[PLAYWIN_AREA], h, w, GAME_YPOS, split); + + /* Status area */ + w = MAX(0, scr_w-split-1); + realloc_area(&state->timer, 1, w, GAME_YPOS+h, split+1); + + /* Toolbar */ + realloc_area(&state->toolbar, 1, scr_w, scr_h-1, 0); + curs_draw_toolbar(state); } static void app_initialize(int argc, char **argv) { + int enable_mouse = 1; int opt; if (argc > 0) @@ -172,6 +306,12 @@ static void app_initialize(int argc, char **argv) while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) { switch (opt) { + case LOPT_MOUSE: + enable_mouse = 2; + break; + case LOPT_NO_MOUSE: + enable_mouse = 0; + break; case LOPT_VERSION: print_version(); exit(EXIT_SUCCESS); @@ -185,16 +325,47 @@ static void app_initialize(int argc, char **argv) } game_reset(&state.board); + state.cursor = -1; initscr(); start_color(); - if (curs_set(0) != ERR) - leaveok(stdscr, TRUE); + curs_set(0); cbreak(); keypad(stdscr, TRUE); - mousemask(BUTTON1_PRESSED, NULL); - mouseinterval(0); + if (enable_mouse) { + /* + * While we only care about a subset of these events, for + * some reason with ncurses failing to enable all of them + * causes timeout to stop working when disabled buttons are + * pressed (or released). + * + * It is not known if other curses have this problem; at least + * pdcurses does not, but the extra events should be harmless + * in any case. + */ +#if HAVE_CURSES_MOUSE_SUPPORT + unsigned long mask = BUTTON1_PRESSED | BUTTON1_RELEASED + | BUTTON2_PRESSED | BUTTON2_RELEASED + | BUTTON3_PRESSED | BUTTON3_RELEASED +#ifdef BUTTON4_PRESSED + | BUTTON4_PRESSED | BUTTON4_RELEASED +#endif +#ifdef BUTTON5_PRESSED + | BUTTON5_PRESSED | BUTTON5_RELEASED +#endif + ; +#if HAVE_CURSES_MOUSE_SET + mouse_set(mask); +#elif HAVE_CURSES_MOUSEMASK + mousemask(mask, NULL); +#endif +#if HAVE_CURSES_MOUSEINTERVAL + mouseinterval(0); +#endif +#endif /* HAVE_CURSES_MOUSE_SUPPORT */ + } + noecho(); init_pair(TILE_RED, COLOR_RED, COLOR_BLACK); @@ -203,34 +374,302 @@ static void app_initialize(int argc, char **argv) init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK); init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK); init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK); + init_pair(RR_COLOUR_SHADOW, COLOR_BLACK, COLOR_BLACK); + init_pair(RR_COLOUR_TOOLBAR, COLOR_CYAN, COLOR_BLACK); - curs_alloc_tiles(&state); + setup_mainwin(&state); refresh(); } -static void do_move(struct app_state *state, int x, int y) +static void update_timer(struct app_state *state, uint_fast32_t ms) +{ + unsigned sec, min; + + state->timer_ms = ms; + sec = ms / 1000; ms %= 1000; + min = sec / 60; sec %= 60; + mvwprintw(state->timer, 0, 0, "Time: %u:%.2u.%.3u", + min, sec, (unsigned)ms); + wclrtoeol(state->timer); + wnoutrefresh(state->timer); +} + +static uint_fast32_t do_move(struct app_state *state, int x, int y) { uint_fast32_t mask; if ((mask = game_do_move(&state->board, x, y)) != 0) { + uint_fast32_t goal = game_check_goal(&state->board); + + if (state->view_goal_on_game) { + state->view_goal_on_game = 0; + mask |= goal; + } + + if (goal == 0) { + /* Solved! */ + update_timer(state, game_finish(&state->board)); + mask |= ~GOAL_MASK; + state->cursor = -1; + timeout(-1); + } + curs_redraw_game(state, mask); - refresh(); + doupdate(); } + + return mask; } -static void do_mouse(struct app_state *state, MEVENT *mev) +static void do_reset_cursor(struct app_state *state) { - if (mev->bstate == BUTTON1_PRESSED) { - int w, h, x, y; + state->cursor = 5*state->board.y + state->board.x; +} - /* Determine size of the game area */ - getmaxyx(state->tile_border, h, w); - w = 2*(w+1)/2; +void curs_new_game(struct app_state *state) +{ + game_reset(&state->board); - if (mev->x < 4 || (x = mev->x - 4)/5 >= w) return; - if (mev->y < 2 || (y = mev->y - 2)/5 >= h) return; + do_reset_cursor(state); + curs_redraw_game(state, -1); + curs_redraw_goal(state, -1); + update_timer(state, 0); + doupdate(); - do_move(state, x/w, y/h); + timeout(33); + game_begin(&state->board); +} + +#if HAVE_CURSES_MOUSE_SUPPORT +/* + * Given x, y as screen coordinates, record which (if any) toolbar function + * label is at that position, to be performed later. + * + * If no function is indicated, returns zero. Otherwise, returns non-zero. + */ +static int press_toolbar(struct app_state *state, int x, int y) +{ + return state->toolbar_click = curs_toolbar_mouse_func(state, x, y); +} + +/* + * Given x, y as screen coordinates, perform the toolbar action. + * + * Perform the action previously recorded by press_toolbar, if and only if + * the x, y screen coordinates correspond to the same function. + */ +static void release_toolbar(struct app_state *state, int x, int y) +{ + int func = curs_toolbar_mouse_func(state, x, y); + + if (func && state->toolbar_click == func) { + curs_execute_function(state, func); + } + + state->toolbar_click = 0; +} + +/* + * Given x, y as screen coordinates, determine which (if any) game tile is + * at that position. + * + * If there is no such tile, performs no action and returns 0. + * + * Otherwise, attempts to move that tile and returns non-zero. + */ +static int press_tile(struct app_state *state, int x, int y) +{ + int game_x, game_y, tile_w, tile_h; + uint_fast32_t cursor_mask, move_mask; + + getbegyx(state->gamewin[PLAYWIN_AREA], game_y, game_x); + getmaxyx(state->gamewin[PLAYWIN_TILEBORDER], tile_h, tile_w); + tile_w += tile_w & 1; + + /* special case the left spacer column */ + if (x == game_x+1) + x++; + + if (x < game_x+2 || (x -= game_x+2)/5 >= tile_w) + return 0; + if (y < game_y+1 || (y -= game_y+1)/5 >= tile_h) + return 0; + + /* OK, selected a tile. */ + x /= tile_w; + y /= tile_h; + + /* Disable the keyboard cursor due to mouse action */ + cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor; + state->cursor = -1; + + move_mask = do_move(state, x, y); + if ((cursor_mask & move_mask) == 0) { + curs_redraw_game(state, cursor_mask); + doupdate(); + } + + return 1; +} + +static void do_mouse(struct app_state *state) +{ + unsigned long bstate; + int x, y; + +#if HAVE_CURSES_GETMOUSE_NCURSES + MEVENT mev; + + if (getmouse(&mev) == ERR) + return; + + x = mev.x, y = mev.y; + bstate = mev.bstate; +#elif HAVE_CURSES_REQUEST_MOUSE_POS + request_mouse_pos(); + x = MOUSE_X_POS; + y = MOUSE_Y_POS; + + bstate = 0; + +#define set_bstate_helper(button) do { if (BUTTON_CHANGED(button)) \ + switch (BUTTON_STATUS(button)) { \ + case BUTTON_RELEASED: bstate |= BUTTON ## button ## _RELEASED; break; \ + case BUTTON_PRESSED: bstate |= BUTTON ## button ## _PRESSED; break; \ + } \ +} while (0); + + set_bstate_helper(1); + set_bstate_helper(3); +#endif + if (bstate & BUTTON3_PRESSED) { + state->view_goal_on_game = 1; + curs_redraw_game(state, game_check_goal(&state->board)); + doupdate(); + } + + if (bstate & BUTTON3_RELEASED) { + state->view_goal_on_game = 0; + curs_redraw_game(state, game_check_goal(&state->board)); + doupdate(); + } + + /* Ignore button1 if holding button3 to view goal */ + if (state->view_goal_on_game & 1) + return; + + if (bstate & BUTTON1_PRESSED) { + if (press_toolbar(state, x, y)); + else if (press_tile(state, x, y)); + } + + if (bstate & BUTTON1_RELEASED) { + release_toolbar(state, x, y); + } +} +#endif /* HAVE_CURSES_MOUSE_SUPPORT */ + +static void do_move_cursor(struct app_state *state, int c) +{ + uint_fast32_t mask = 0; + + if (state->cursor < 0) { + /* Cursor was hidden; reset it */ + do_reset_cursor(state); + } else { + mask = 1ul << state->cursor; + } + + if (state->view_goal_on_game) { + state->view_goal_on_game = 0; + mask |= game_check_goal(&state->board); + } + + switch (c) { + case KEY_UP: + if ((state->cursor -= 5) < 0) + state->cursor += 25; + break; + case KEY_DOWN: + if ((state->cursor += 5) >= 25) + state->cursor -= 25; + break; + case KEY_LEFT: + if ((state->cursor -= 1) % 5 == 4 || state->cursor < 0) + state->cursor += 5; + break; + case KEY_RIGHT: + if ((state->cursor += 1) % 5 == 0) + state->cursor -= 5; + break; + } + + curs_redraw_game(state, mask | 1ul << state->cursor); + doupdate(); +} + +static void do_keystroke(struct app_state *state, int c) +{ + int last_input = state->last_input; + state->last_input = c; + + switch (c) { + case KEY_DOWN: case KEY_UP: case KEY_LEFT: case KEY_RIGHT: + do_move_cursor(state, c); + break; + case '\t': + state->view_goal_on_game ^= 2u; + curs_redraw_game(state, game_check_goal(&state->board)); + doupdate(); + break; + case ' ': + if (!(state->view_goal_on_game & 1) && state->cursor >= 0) + do_move(state, state->cursor%5, state->cursor/5); + break; + } + + /* ESC+# keys */ + if (last_input == '\33' && c >= '0' && c <= '9') { + curs_execute_function(state, (c -= '0') == 0 ? 10 : c); + } + + /* F# keys */ + if (c >= KEY_F(1) && c <= KEY_F(10)) { + curs_execute_function(state, c - KEY_F0); + } +} + +/* One iteration of main input loop */ +void do_mainloop(struct app_state *state) +{ + int c = getch(); + + switch (c) { +#ifdef KEY_RESIZE + case KEY_RESIZE: + setup_mainwin(state); + clear(); + refresh(); + curs_redraw_game(state, -1); + curs_redraw_goal(state, -1); + curs_draw_toolbar(state); + update_timer(state, state->timer_ms); + doupdate(); + break; +#endif +#if HAVE_CURSES_MOUSE_SUPPORT + case KEY_MOUSE: + do_mouse(state); + break; +#endif + default: + do_keystroke(state, c); + case ERR:; + } + + if (state->board.x <= 4) { + update_timer(state, game_elapsed(&state->board)); + doupdate(); } } @@ -239,25 +678,8 @@ int main(int argc, char **argv) setlocale(LC_ALL, ""); app_initialize(argc, argv); - curs_redraw_game(&state, -1); - refresh(); - - while (1) { - int c = getch(); - MEVENT mev; - - switch (c) { - case KEY_RESIZE: - curs_alloc_tiles(&state); - clear(); - refresh(); - curs_redraw_game(&state, -1); - refresh(); - break; - case KEY_MOUSE: - if (getmouse(&mev) != ERR) - do_mouse(&state, &mev); - break; - } - } + curs_new_game(&state); + while (1) + do_mainloop(&state); + abort(); }