]> git.draconx.ca Git - rrace.git/blobdiff - src/curses.c
curses: Begin to implement a game menu system.
[rrace.git] / src / curses.c
index 6d86101c41f354236ce6f82fe3a694916613ae84..7497395cbaa4f4865f18faf3ea979113a1935bad 100644 (file)
@@ -45,12 +45,32 @@ enum {
        WINDOW_MAX,
 };
 
+/* Colour pair enumeration */
+enum {
+       /* Pairs 1-6 correspond to tile colours */
+       RR_COLOUR_CURSOR = TILE_MAX, // black on black, for the cursor
+       RR_COLOUR_TOOLBAR,           // cyan on black (use reverse video)
+       RR_COLOUR_MAX
+};
+
 static struct app_state {
        struct board board;
 
        WINDOW *gamewin[WINDOW_MAX], *goalwin[WINDOW_MAX];
-       WINDOW *timer;
-       int cursor;
+       WINDOW *toolbar, *timer;
+       int last_input;
+
+       /* 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 void print_version(void)
@@ -103,7 +123,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_CURSOR);
        }
 
        getmaxyx(border, h, w);
@@ -171,12 +191,20 @@ static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
 
 static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
 {
-       uint_least32_t *gp = state->board.game;
+       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) {
                        redraw_tile(state->gamewin, i%5, i/5,
@@ -257,6 +285,58 @@ static void realloc_tiles(WINDOW **win, int h)
        win[WINDOW_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
 }
 
+/*
+ * Given the toolbar function number (between 1 and 10, inclusive), and the
+ * total width of the screen, return the character position for the start of
+ * its label display.
+ *
+ * The intention is to divide the width of the screen into 10 roughly
+ * equally-sized areas, spreading out the remainder so that the width of
+ * each label is a monotone increasing function of the total width.
+ *
+ * The minimum size of a label is 6 characters (2 of which are used for the
+ * number indicator)
+ */
+static int toolbar_xpos(int i, int total_width)
+{
+       int button_width = MAX(6, total_width/10);
+       int rem = total_width - 10*button_width;
+       int pos = (i-1)*button_width;
+
+       switch (rem) {
+       case 9: pos += i > 6;
+       case 8: pos += i > 2;
+       case 7: pos += i > 7;
+       case 6: pos += i > 3;
+       case 5: pos += i > 8;
+       case 4: pos += i > 4;
+       case 3: pos += i > 9;
+       case 2: pos += i > 5;
+       }
+
+       return pos;
+}
+
+static void draw_toolbar(struct app_state *state)
+{
+       WINDOW *toolbar = state->toolbar;
+       int i, w, lw;
+
+       getmaxyx(toolbar, i, w);
+       werase(toolbar);
+
+       lw = MAX(6, w/10);
+       mvwprintw(toolbar, 0, toolbar_xpos( 1, w)+2, "%.*s", lw, "Help");
+       mvwprintw(toolbar, 0, toolbar_xpos( 2, w)+2, "%.*s", lw, "NewGame");
+       mvwprintw(toolbar, 0, toolbar_xpos(10, w)+2, "%.*s", lw, "Exit");
+
+       mvwchgat(toolbar, 0, 0, -1, A_REVERSE, RR_COLOUR_TOOLBAR, 0);
+       for (i = 1; i <= 10; i++)
+               mvwprintw(toolbar, 0, toolbar_xpos(i, w), "%2d", i);
+
+       wnoutrefresh(state->toolbar);
+}
+
 static void setup_mainwin(struct app_state *state)
 {
        int w, h, gamesz, goalsz, scr_w, scr_h, split;
@@ -293,6 +373,10 @@ static void setup_mainwin(struct app_state *state)
        /* 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);
+       draw_toolbar(state);
 }
 
 static void app_initialize(int argc, char **argv)
@@ -373,17 +457,46 @@ 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_CURSOR, COLOR_BLACK, COLOR_BLACK);
+       init_pair(RR_COLOUR_TOOLBAR, COLOR_CYAN, COLOR_BLACK);
 
        setup_mainwin(&state);
        refresh();
 }
 
+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);
+       wrefresh(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);
                doupdate();
        }
@@ -396,18 +509,6 @@ static void do_reset_cursor(struct app_state *state)
        state->cursor = 5*state->board.y + state->board.x;
 }
 
-static void update_timer(struct app_state *state, uint_fast32_t ms)
-{
-       unsigned sec, min;
-
-       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);
-       wrefresh(state->timer);
-}
-
 static void do_new_game(struct app_state *state)
 {
        game_reset(&state->board);
@@ -422,7 +523,111 @@ static void do_new_game(struct app_state *state)
        game_begin(&state->board);
 }
 
+static void do_function(struct app_state *state, unsigned func)
+{
+       switch (func) {
+       case 2:
+               do_new_game(state);
+               break;
+       case 10:
+               endwin();
+               exit(0);
+       }
+}
+
 #if HAVE_CURSES_MOUSE_SUPPORT
+/*
+ * Returns the toolbar function (1 through 10) under the given x, y screen
+ * coordinates, or 0 if the coordinates are outside of the toolbar.
+ */
+static int mouse_toolbar_function(struct app_state *state, int x, int y)
+{
+       int toolbar_x, toolbar_y, toolbar_w, toolbar_h, i;
+
+       getbegyx(state->toolbar, toolbar_y, toolbar_x);
+       getmaxyx(state->toolbar, toolbar_h, toolbar_w);
+
+       if ((void)toolbar_x, y != toolbar_y)
+               return 0;
+
+       /* OK, selected a button, determine which one */
+       for (i = 10; i > 1; i--) {
+               if ((void)toolbar_h, x >= toolbar_xpos(i, toolbar_w))
+                       break;
+       }
+
+       return i;
+}
+
+/*
+ * 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 = mouse_toolbar_function(state, x, y);
+}
+
+/*
+ * 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 = mouse_toolbar_function(state, x, y);
+
+       if (func && state->toolbar_click == func) {
+               do_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[WINDOW_AREA], game_y, game_x);
+       getmaxyx(state->gamewin[WINDOW_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;
@@ -453,37 +658,47 @@ static void do_mouse(struct app_state *state)
        set_bstate_helper(1);
        set_bstate_helper(3);
 #endif
-       if (bstate == BUTTON1_PRESSED) {
-               uint_fast32_t cursor_mask, move_mask;
-               int w, h;
+       if (bstate & BUTTON3_PRESSED) {
+               state->view_goal_on_game = 1;
+               curs_redraw_game(state, game_check_goal(&state->board));
+               doupdate();
+       }
 
-               /* Determine size of the game area */
-               getmaxyx(state->gamewin[WINDOW_TILEBORDER], h, w);
-               w = 2*(w+1)/2;
+       if (bstate & BUTTON3_RELEASED) {
+               state->view_goal_on_game = 0;
+               curs_redraw_game(state, game_check_goal(&state->board));
+               doupdate();
+       }
 
-               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)
 {
-       uint_fast32_t mask = 1ul << state->cursor;
+       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) {
@@ -506,53 +721,78 @@ static void do_move_cursor(struct app_state *state, int c)
        }
 
        curs_redraw_game(state, mask | 1ul << state->cursor);
-       refresh();
+       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->cursor >= 0)
+               if (!(state->view_goal_on_game & 1) && state->cursor >= 0)
                        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') {
+               do_function(state, (c -= '0') == 0 ? 10 : c);
+       }
 
-       do_new_game(&state);
+       /* F# keys */
+       if (c >= KEY_F(1) && c <= KEY_F(10)) {
+               do_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);
-                       refresh();
-                       break;
+       case KEY_RESIZE:
+               setup_mainwin(state);
+               clear();
+               refresh();
+               curs_redraw_game(state, -1);
+               curs_redraw_goal(state, -1);
+               draw_toolbar(state);
+               update_timer(state, state->timer_ms);
+               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:;
-               }
-
-               update_timer(&state, game_elapsed(&state.board));
+       default:
+               do_keystroke(state, c);
+       case ERR:;
        }
+
+       if (state->board.x <= 4)
+               update_timer(state, game_elapsed(&state->board));
+}
+
+int main(int argc, char **argv)
+{
+       setlocale(LC_ALL, "");
+       app_initialize(argc, argv);
+
+       do_new_game(&state);
+       while (1)
+               do_mainloop(&state);
+       abort();
 }