X-Git-Url: http://git.draconx.ca/gitweb/rrace.git/blobdiff_plain/1549df40a72face9c7fbd4e5d96386ba2a07bb58..46cf1b673efc9fb9cb84c70a86024d76762c3444:/src/curses.c diff --git a/src/curses.c b/src/curses.c index 06ec351..dc870de 100644 --- a/src/curses.c +++ b/src/curses.c @@ -1,6 +1,6 @@ /* * Curses UI for slide puzzle game - * Copyright © 2022 Nick Bowler + * Copyright © 2022-2024 Nick Bowler * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,6 +17,7 @@ */ #include +#include #include #include #include @@ -24,45 +25,19 @@ #include #include "help.h" +#include "xtra.h" #include "version.h" -#include "cursesopt.h" -#include "game.h" -#define MIN(a, b) ((a) < (b) ? (a) : (b)) -#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#include "cursesui.h" +#include "cursesopt.h" enum { - GAME_YPOS = 1 // top row of game and goal areas. + GAME_YPOS = 1 /* top row of game and goal areas. */ }; static const char *progname = "rrace"; -static const struct option lopts[] = { LOPTS_INITIALIZER, {0} }; - -enum { - WINDOW_TILEBORDER, - WINDOW_TILEFILL, - WINDOW_AREA, - WINDOW_MAX, -}; - -static struct app_state { - struct board board; - WINDOW *gamewin[WINDOW_MAX], *goalwin[WINDOW_MAX]; - WINDOW *timer; - - /* Most recently displayed timer value, for screen redraw. */ - uint_least32_t timer_ms; - - /* Location of the keyboard cursor */ - int_least8_t cursor; - - /* If true, the goal will be displayed over the main play area. */ - uint_least8_t view_goal_on_game; - - /* Clicked toolbar item */ - uint_least8_t toolbar_click; -} state; +static struct app_state state; static void print_version(void) { @@ -79,7 +54,7 @@ static void print_usage(FILE *f) fprintf(f, "Try %s --help for more information.\n", progname); } -static void print_help(void) +static void print_help(const struct option *lopts) { struct lopt_help help = {0}; const struct option *opt; @@ -101,7 +76,7 @@ static void print_help(void) static void draw_tile(WINDOW **win, unsigned colour, unsigned selected, unsigned x, unsigned y, unsigned start_column) { - WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL]; + WINDOW *border = win[PLAYWIN_TILEBORDER], *fill = win[PLAYWIN_TILEFILL]; int w, h, attr, ch, bc = selected ? '#' : 0; assert(colour < TILE_MAX); @@ -114,7 +89,7 @@ static void draw_tile(WINDOW **win, unsigned colour, unsigned selected, 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(TILE_MAX); + case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(RR_COLOUR_SHADOW); } getmaxyx(border, h, w); @@ -141,17 +116,11 @@ static void draw_tile(WINDOW **win, unsigned colour, unsigned selected, 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_least32_t *gp, unsigned selected) { - uint_fast32_t pos = board_position(x, y); - unsigned char tile = 0; + unsigned tile = board_tile(gp, 5*y+x); - if (bit0 & pos) tile |= 1; - if (bit1 & pos) tile |= 2; - if (bit2 & pos) tile |= 4; assert(tile < TILE_MAX); - draw_tile(win, tile, selected, x, y, start_column); return tile; } @@ -159,7 +128,7 @@ redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column, 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[WINDOW_AREA]; + WINDOW *area = win[PLAYWIN_AREA]; getmaxyx(stdscr, h, w); @@ -188,19 +157,13 @@ static void curs_redraw_game(struct app_state *state, uint_fast32_t mask) 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; - } + if (state->view_goal_on_game) + gp = game_overlay_goal(&state->board, buf); for (i = 0; i < 25; i++) { if (mask & 1) { redraw_tile(state->gamewin, i%5, i/5, - 4, gp[0], gp[1], gp[2], - i == state->cursor); + 4, gp, i == state->cursor); } mask >>= 1; } @@ -208,36 +171,47 @@ static void curs_redraw_game(struct app_state *state, uint_fast32_t mask) static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask) { - uint_least16_t *gp = state->board.goal; + uint_least32_t gp[3] = { + state->board.goal[0], + state->board.goal[1], + state->board.goal[2] + }; int i, x, y; - if (!state->goalwin[WINDOW_AREA]) + if (!state->goalwin[PLAYWIN_AREA]) return; - getbegyx(state->goalwin[WINDOW_AREA], y, x); + getbegyx(state->goalwin[PLAYWIN_AREA], y, x); if (mask == -1) redraw_area_border(state->goalwin, x, 3); 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); + redraw_tile(state->goalwin, i%3, i/3, x+2, gp, 0); } mask >>= 1; } } +/* Thin wrapper around wresize which returns ERR if support is unavailable. */ +static int do_wresize(WINDOW *window, int h, int w) +{ +#if HAVE_CURSES_WRESIZE + return wresize(window, h, w); +#endif + return ERR; +} + static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x) { WINDOW *win = *orig; if (win) { -#if HAVE_CURSES_WRESIZE - if (wresize(win, h, w) != ERR) { + if (do_wresize(win, h, w) != ERR) { mvwin(win, y, x); return win; } -#endif + delwin(win); } @@ -248,19 +222,15 @@ static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x) static void realloc_tiles(WINDOW **win, int h) { - WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL]; + WINDOW *border = win[PLAYWIN_TILEBORDER], *fill = win[PLAYWIN_TILEFILL]; int w = 2*h - 1; if (fill && border) { int old_w, old_h; -#if HAVE_CURSES_WRESIZE - if (wresize(fill, h-2, w-2) != ERR - && wresize(border, h, w) != ERR) - { + if (do_wresize(fill, h-2, w-2) != ERR + && do_wresize(border, h, w) != ERR) return; - } -#endif getmaxyx(border, old_h, old_w); if (old_h == h) @@ -272,8 +242,8 @@ static void realloc_tiles(WINDOW **win, int h) if (border) delwin(border); - win[WINDOW_TILEBORDER] = border = newwin(h, w, 0, 0); - win[WINDOW_TILEFILL] = derwin(border, h-2, w-2, 1, 1); + 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) @@ -302,16 +272,20 @@ static void setup_mainwin(struct app_state *state) /* 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[WINDOW_AREA], h, w, GAME_YPOS, 2); + 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[WINDOW_AREA], h, w, GAME_YPOS, split); + 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) @@ -319,6 +293,8 @@ static void app_initialize(int argc, char **argv) int enable_mouse = 1; int opt; + XTRA_PACKED_LOPTS(lopts); + if (argc > 0) progname = argv[0]; @@ -334,7 +310,7 @@ static void app_initialize(int argc, char **argv) print_version(); exit(EXIT_SUCCESS); case LOPT_HELP: - print_help(); + print_help(lopts); exit(EXIT_SUCCESS); default: print_usage(stderr); @@ -392,7 +368,8 @@ 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(TILE_MAX, COLOR_BLACK, COLOR_BLACK); + init_pair(RR_COLOUR_SHADOW, COLOR_BLACK, COLOR_BLACK); + init_pair(RR_COLOUR_TOOLBAR, COLOR_CYAN, COLOR_BLACK); setup_mainwin(&state); refresh(); @@ -408,7 +385,7 @@ static void update_timer(struct app_state *state, uint_fast32_t ms) mvwprintw(state->timer, 0, 0, "Time: %u:%.2u.%.3u", min, sec, (unsigned)ms); wclrtoeol(state->timer); - wrefresh(state->timer); + wnoutrefresh(state->timer); } static uint_fast32_t do_move(struct app_state *state, int x, int y) @@ -443,11 +420,12 @@ static void do_reset_cursor(struct app_state *state) state->cursor = 5*state->board.y + state->board.x; } -static void do_new_game(struct app_state *state) +void curs_new_game(struct app_state *state) { game_reset(&state->board); do_reset_cursor(state); + state->view_goal_on_game = 0; curs_redraw_game(state, -1); curs_redraw_goal(state, -1); update_timer(state, 0); @@ -458,6 +436,77 @@ static void do_new_game(struct app_state *state) } #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; @@ -500,29 +549,20 @@ static void do_mouse(struct app_state *state) doupdate(); } - if (!state->view_goal_on_game && bstate & BUTTON1_PRESSED) { - uint_fast32_t cursor_mask, move_mask; - int w, h; - - /* Determine size of the game area */ - getmaxyx(state->gamewin[WINDOW_TILEBORDER], h, w); - w = 2*(w+1)/2; - - if (x < 4 || (x -= 4)/5 >= w) return; - if (y <= GAME_YPOS || (y -= GAME_YPOS+1)/5 >= h) return; + /* Ignore button1 if holding button3 to view goal */ + if (state->view_goal_on_game & 1) + return; - /* Turn off the keyboard cursor when using the mouse */ - cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor; - state->cursor = -1; + if (bstate & BUTTON1_PRESSED) { + if (press_toolbar(state, x, y)); + else if (press_tile(state, x, y)); + } - move_mask = do_move(state, x/w, y/h); - if ((cursor_mask & move_mask) == 0) { - curs_redraw_game(state, cursor_mask); - doupdate(); - } + if (bstate & BUTTON1_RELEASED) { + release_toolbar(state, x, y); } } -#endif +#endif /* HAVE_CURSES_MOUSE_SUPPORT */ static void do_move_cursor(struct app_state *state, int c) { @@ -565,6 +605,9 @@ static void do_move_cursor(struct app_state *state, int c) 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); @@ -579,40 +622,59 @@ static void do_keystroke(struct app_state *state, int c) do_move(state, state->cursor%5, state->cursor/5); break; } -} -int main(int argc, char **argv) -{ - setlocale(LC_ALL, ""); - app_initialize(argc, argv); + /* ESC+# keys */ + if (last_input == '\33' && c >= '0' && c <= '9') { + curs_execute_function(state, (c -= '0') == 0 ? 10 : c); + } - do_new_game(&state); + /* F# keys */ + if (c >= KEY_F(1) && c <= KEY_F(10)) { + curs_execute_function(state, c - KEY_F0); + } +} - while (1) { - int c = getch(); +/* One iteration of main input loop */ +void do_mainloop(struct app_state *state) +{ + int c = getch(); - switch (c) { + switch (c) { #ifdef KEY_RESIZE - case KEY_RESIZE: - setup_mainwin(&state); - clear(); - refresh(); - curs_redraw_game(&state, -1); - curs_redraw_goal(&state, -1); - update_timer(&state, state.timer_ms); - break; + 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; + case KEY_MOUSE: + do_mouse(state); + break; #endif - default: - do_keystroke(&state, c); - case ERR:; - } + default: + do_keystroke(state, c); + case ERR:; + } - if (state.board.x <= 4) - update_timer(&state, game_elapsed(&state.board)); + if (state->board.x <= 4) { + update_timer(state, game_elapsed(&state->board)); + doupdate(); } } + +int main(int argc, char **argv) +{ + setlocale(LC_ALL, ""); + app_initialize(argc, argv); + + curs_new_game(&state); + while (1) + do_mainloop(&state); + abort(); +}