]> git.draconx.ca Git - rrace.git/blobdiff - src/curses.c
curses: Make the game winnable.
[rrace.git] / src / curses.c
index eecf3a19e736a6f9e13552eea465fe130a9fb5dc..06ec3519c87e530bcaa4837c7ff7276c882bab47 100644 (file)
 #define MIN(a, b) ((a) < (b) ? (a) : (b))
 #define MAX(a, b) ((a) > (b) ? (a) : (b))
 
+enum {
+       GAME_YPOS = 1 // top row of game and goal areas.
+};
+
 static const char *progname = "rrace";
 static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
 
@@ -45,6 +49,19 @@ 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 void print_version(void)
@@ -81,12 +98,11 @@ static void print_help(void)
        printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
 }
 
-static void draw_tile(WINDOW **win, unsigned colour,
+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];
-       int attr, ch;
-       int w, h;
+       int w, h, attr, ch, bc = selected ? '#' : 0;
 
        assert(colour < TILE_MAX);
        attr = COLOR_PAIR(colour);
@@ -97,18 +113,17 @@ static void draw_tile(WINDOW **win, unsigned colour,
        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(TILE_MAX);
        }
 
        getmaxyx(border, h, w);
        w = 2*(w+1)/2;
 
-       if (mvwin(border, 2+h*y, start_column+w*x) == ERR)
+       if (mvwin(border, 1+GAME_YPOS+h*y, start_column+w*x) == ERR)
                return;
 
        if (colour != TILE_EMPTY) {
-               wattrset(border, attr);
-               box(border, 0, 0);
-
                mvderwin(fill, 1, 1);
                wbkgdset(fill, A_REVERSE|attr|ch);
                werase(fill);
@@ -116,12 +131,18 @@ static void draw_tile(WINDOW **win, unsigned colour,
                werase(border);
        }
 
+       if (bc || colour) {
+               wattrset(border, attr);
+               wborder(border, bc, bc, bc, bc, bc, bc, bc, bc);
+       }
+
        wnoutrefresh(border);
 }
 
 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)
+           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;
@@ -131,7 +152,7 @@ redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column,
        if (bit2 & pos) tile |= 4;
        assert(tile < TILE_MAX);
 
-       draw_tile(win, tile, x, y, start_column);
+       draw_tile(win, tile, selected, x, y, start_column);
        return tile;
 }
 
@@ -142,9 +163,9 @@ static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
 
        getmaxyx(stdscr, h, w);
 
-       if (h <= 2) {
+       if (h <= GAME_YPOS+1) {
                bl = ACS_ULCORNER, br = ACS_URCORNER;
-       } else if (h <= 3*sz+2) {
+       } else if (h <= 3*sz+GAME_YPOS+1) {
                bl = br = ACS_VLINE, bs = ' ';
        }
 
@@ -161,16 +182,25 @@ 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,
-                                   4, gp[0], gp[1], gp[2]);
+                                   4, gp[0], gp[1], gp[2],
+                                   i == state->cursor);
                }
                mask >>= 1;
        }
@@ -191,7 +221,7 @@ static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask)
        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]);
+                                   x+2, gp[0], gp[1], gp[2], 0);
                }
                mask >>= 1;
        }
@@ -271,13 +301,17 @@ static void setup_mainwin(struct app_state *state)
 
        /* Frame for game area */
        w = MIN(scr_w-2, 3+10*gamesz);
-       h = MIN(scr_h-1, 2+5*gamesz);
-       realloc_area(&state->gamewin[WINDOW_AREA], h, w, 1, 2);
+       h = MIN(scr_h-GAME_YPOS, 2+5*gamesz);
+       realloc_area(&state->gamewin[WINDOW_AREA], h, w, GAME_YPOS, 2);
 
        /* Frame for goal area */
        w = MIN(scr_w-split, 3+6*goalsz);
-       h = MIN(scr_h-1, 2+3*goalsz);
-       realloc_area(&state->goalwin[WINDOW_AREA], h, w, 1, split);
+       h = MIN(scr_h-GAME_YPOS, 2+3*goalsz);
+       realloc_area(&state->goalwin[WINDOW_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);
 }
 
 static void app_initialize(int argc, char **argv)
@@ -309,23 +343,45 @@ 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);
        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(BUTTON1_PRESSED);
+               mouse_set(mask);
 #elif HAVE_CURSES_MOUSEMASK
-               mousemask(BUTTON1_PRESSED, NULL);
+               mousemask(mask, NULL);
 #endif
 #if HAVE_CURSES_MOUSEINTERVAL
                mouseinterval(0);
 #endif
+#endif /* HAVE_CURSES_MOUSE_SUPPORT */
        }
 
        noecho();
@@ -336,19 +392,69 @@ 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);
 
        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);
+       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);
-               refresh();
+               doupdate();
        }
+
+       return mask;
+}
+
+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)
+{
+       game_reset(&state->board);
+
+       do_reset_cursor(state);
+       curs_redraw_game(state, -1);
+       curs_redraw_goal(state, -1);
+       update_timer(state, 0);
+       doupdate();
+
+       timeout(33);
+       game_begin(&state->board);
 }
 
 #if HAVE_CURSES_MOUSE_SUPPORT
@@ -371,14 +477,31 @@ static void do_mouse(struct app_state *state)
        y = MOUSE_Y_POS;
 
        bstate = 0;
-       if (BUTTON_CHANGED(1)) {
-               switch (BUTTON_STATUS(1)) {
-               case BUTTON_RELEASED: bstate |= BUTTON1_RELEASED;
-               case BUTTON_PRESSED:  bstate |= BUTTON1_PRESSED;
-               }
-       }
+
+#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 == BUTTON1_PRESSED) {
+       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();
+       }
+
+       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 */
@@ -386,21 +509,84 @@ static void do_mouse(struct app_state *state)
                w = 2*(w+1)/2;
 
                if (x < 4 || (x -= 4)/5 >= w) return;
-               if (y < 2 || (y -= 2)/5 >= h) return;
+               if (y <= GAME_YPOS || (y -= GAME_YPOS+1)/5 >= h) return;
+
+               /* Turn off the keyboard cursor when using the mouse */
+               cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor;
+               state->cursor = -1;
 
-               do_move(state, x/w, y/h);
+               move_mask = do_move(state, x/w, y/h);
+               if ((cursor_mask & move_mask) == 0) {
+                       curs_redraw_game(state, cursor_mask);
+                       doupdate();
+               }
        }
 }
 #endif
 
+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)
+{
+       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;
+       }
+}
+
 int main(int argc, char **argv)
 {
        setlocale(LC_ALL, "");
        app_initialize(argc, argv);
 
-       curs_redraw_game(&state, -1);
-       curs_redraw_goal(&state, -1);
-       refresh();
+       do_new_game(&state);
 
        while (1) {
                int c = getch();
@@ -413,7 +599,7 @@ int main(int argc, char **argv)
                        refresh();
                        curs_redraw_game(&state, -1);
                        curs_redraw_goal(&state, -1);
-                       refresh();
+                       update_timer(&state, state.timer_ms);
                        break;
 #endif
 #if HAVE_CURSES_MOUSE_SUPPORT
@@ -421,6 +607,12 @@ int main(int argc, char **argv)
                        do_mouse(&state);
                        break;
 #endif
+               default:
+                       do_keystroke(&state, c);
+               case ERR:;
                }
+
+               if (state.board.x <= 4)
+                       update_timer(&state, game_elapsed(&state.board));
        }
 }