]> git.draconx.ca Git - rrace.git/blobdiff - src/curses.c
Fix bitmap generation in goal overlay feature.
[rrace.git] / src / curses.c
index bf97caf5ab951a9bc213c96e1ffeea5534a6bfd9..c0f4265e25fc98054f947cce25290b227e941ab6 100644 (file)
@@ -1,6 +1,6 @@
 /*
  * Curses UI for slide puzzle game
- * Copyright © 2022 Nick Bowler
+ * Copyright © 2022-2023 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
 
 #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,209 @@ 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_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 (state->board.game[0] & pos) tile |= 1;
-       if (state->board.game[1] & pos) tile |= 2;
-       if (state->board.game[2] & 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)
+               gp = game_overlay_goal(&state->board, 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, 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_least32_t gp[3] = {
+               state->board.goal[0],
+               state->board.goal[1],
+               state->board.goal[2]
+       };
+       int i, x, y;
+
+       if (!state->goalwin[PLAYWIN_AREA])
+               return;
 
-       getmaxyx(stdscr, h, w);
-       tilesz = (h - 4) / 5;
-       if (tilesz < 3)
-               tilesz = 3;
+       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);
+               }
+               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 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 (do_wresize(win, h, w) != ERR) {
+                       mvwin(win, y, x);
+                       return win;
+               }
+
+               delwin(win);
+       }
+
+       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 (state->tile_border) {
-               delwin(state->tile_fill);
-               delwin(state->tile_border);
+       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 +297,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 +316,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 +365,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 +669,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();
 }