2 * Curses UI for slide puzzle game
3 * Copyright © 2022 Nick Bowler
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
28 #include "cursesopt.h"
31 #define MIN(a, b) ((a) < (b) ? (a) : (b))
32 #define MAX(a, b) ((a) > (b) ? (a) : (b))
35 GAME_YPOS = 1 // top row of game and goal areas.
38 static const char *progname = "rrace";
39 static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
48 /* Colour pair enumeration */
50 /* Pairs 1-6 correspond to tile colours */
51 RR_COLOUR_CURSOR = TILE_MAX, // black on black, for the cursor
52 RR_COLOUR_TOOLBAR, // cyan on black (use reverse video)
56 static struct app_state {
59 WINDOW *gamewin[WINDOW_MAX], *goalwin[WINDOW_MAX];
60 WINDOW *toolbar, *timer;
63 /* Most recently displayed timer value, for screen redraw. */
64 uint_least32_t timer_ms;
66 /* Location of the keyboard cursor */
69 /* If true, the goal will be displayed over the main play area. */
70 uint_least8_t view_goal_on_game;
72 /* Clicked toolbar item */
73 uint_least8_t toolbar_click;
76 static void print_version(void)
78 version_print_head("rrace-curses", stdout);
79 puts("License GPLv3+: GNU GPL version 3 or any later version");
80 puts("This is free software: you are free to change and redistribute it.");
81 puts("There is NO WARRANTY, to the extent permitted by law.");
84 static void print_usage(FILE *f)
86 fprintf(f, "Usage: %s [options]\n", progname);
88 fprintf(f, "Try %s --help for more information.\n", progname);
91 static void print_help(void)
93 struct lopt_help help = {0};
94 const struct option *opt;
100 for (opt = lopts; opt->name; opt++) {
101 if (!lopt_get_help(opt, &help))
103 help_print_option(opt, help.arg, help.desc, 20);
107 printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
110 static void draw_tile(WINDOW **win, unsigned colour, unsigned selected,
111 unsigned x, unsigned y, unsigned start_column)
113 WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
114 int w, h, attr, ch, bc = selected ? '#' : 0;
116 assert(colour < TILE_MAX);
117 attr = COLOR_PAIR(colour);
119 case TILE_RED: ch = 'X'; break;
120 case TILE_ORANGE: ch = '|'; break;
121 case TILE_GREEN: ch = '+'; break;
122 case TILE_YELLOW: ch = '~'; attr |= A_BOLD; break;
123 case TILE_BLUE: ch = 'o'; attr |= A_BOLD; break;
124 case TILE_WHITE: ch = '.'; attr |= A_BOLD; break;
126 case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(RR_COLOUR_CURSOR);
129 getmaxyx(border, h, w);
132 if (mvwin(border, 1+GAME_YPOS+h*y, start_column+w*x) == ERR)
135 if (colour != TILE_EMPTY) {
136 mvderwin(fill, 1, 1);
137 wbkgdset(fill, A_REVERSE|attr|ch);
144 wattrset(border, attr);
145 wborder(border, bc, bc, bc, bc, bc, bc, bc, bc);
148 wnoutrefresh(border);
152 redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column,
153 uint_fast32_t bit0, uint_fast32_t bit1, uint_fast32_t bit2,
156 uint_fast32_t pos = board_position(x, y);
157 unsigned char tile = 0;
159 if (bit0 & pos) tile |= 1;
160 if (bit1 & pos) tile |= 2;
161 if (bit2 & pos) tile |= 4;
162 assert(tile < TILE_MAX);
164 draw_tile(win, tile, selected, x, y, start_column);
168 static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
170 int w, h, tr = 0, rs = 0, br = 0, bs = 0, bl = 0;
171 WINDOW *area = win[WINDOW_AREA];
173 getmaxyx(stdscr, h, w);
175 if (h <= GAME_YPOS+1) {
176 bl = ACS_ULCORNER, br = ACS_URCORNER;
177 } else if (h <= 3*sz+GAME_YPOS+1) {
178 bl = br = ACS_VLINE, bs = ' ';
182 tr = ACS_ULCORNER, br = ACS_LLCORNER;
183 } else if (w <= 6*sz+x+2) {
184 tr = ACS_HLINE, rs = ' ';
185 br = br ? ' ' : ACS_HLINE;
188 wborder(area, 0, rs, 0, bs, 0, tr, bl, br);
192 static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
194 uint_least32_t buf[3], *gp = state->board.game;
198 redraw_area_border(state->gamewin, 2, 5);
200 if (state->view_goal_on_game) {
201 for (i = 0; i < 3; i++) {
202 buf[i] = state->board.goal[i];
203 buf[i] = (gp[i] & ~GOAL_MASK) | (buf[i] << GOAL_SHIFT);
208 for (i = 0; i < 25; i++) {
210 redraw_tile(state->gamewin, i%5, i/5,
211 4, gp[0], gp[1], gp[2],
218 static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask)
220 uint_least16_t *gp = state->board.goal;
223 if (!state->goalwin[WINDOW_AREA])
226 getbegyx(state->goalwin[WINDOW_AREA], y, x);
228 redraw_area_border(state->goalwin, x, 3);
230 for (i = 0; i < 9; i++) {
232 redraw_tile(state->goalwin, i%3, i/3,
233 x+2, gp[0], gp[1], gp[2], 0);
239 static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x)
244 #if HAVE_CURSES_WRESIZE
245 if (wresize(win, h, w) != ERR) {
254 return *orig = subwin(stdscr, h, w, y, x);
258 static void realloc_tiles(WINDOW **win, int h)
260 WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
263 if (fill && border) {
266 #if HAVE_CURSES_WRESIZE
267 if (wresize(fill, h-2, w-2) != ERR
268 && wresize(border, h, w) != ERR)
274 getmaxyx(border, old_h, old_w);
284 win[WINDOW_TILEBORDER] = border = newwin(h, w, 0, 0);
285 win[WINDOW_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
289 * Given the toolbar function number (between 1 and 10, inclusive), and the
290 * total width of the screen, return the character position for the start of
293 * The intention is to divide the width of the screen into 10 roughly
294 * equally-sized areas, spreading out the remainder so that the width of
295 * each label is a monotone increasing function of the total width.
297 * The minimum size of a label is 6 characters (2 of which are used for the
300 static int toolbar_xpos(int i, int total_width)
302 int button_width = MAX(6, total_width/10);
303 int rem = total_width - 10*button_width;
304 int pos = (i-1)*button_width;
307 case 9: pos += i > 6;
308 case 8: pos += i > 2;
309 case 7: pos += i > 7;
310 case 6: pos += i > 3;
311 case 5: pos += i > 8;
312 case 4: pos += i > 4;
313 case 3: pos += i > 9;
314 case 2: pos += i > 5;
320 static void draw_toolbar(struct app_state *state)
322 WINDOW *toolbar = state->toolbar;
325 getmaxyx(toolbar, i, w);
329 mvwprintw(toolbar, 0, toolbar_xpos( 1, w)+2, "%.*s", lw, "Help");
330 mvwprintw(toolbar, 0, toolbar_xpos( 2, w)+2, "%.*s", lw, "NewGame");
331 mvwprintw(toolbar, 0, toolbar_xpos(10, w)+2, "%.*s", lw, "Exit");
333 mvwchgat(toolbar, 0, 0, -1, A_REVERSE, RR_COLOUR_TOOLBAR, 0);
334 for (i = 1; i <= 10; i++)
335 mvwprintw(toolbar, 0, toolbar_xpos(i, w), "%2d", i);
337 wnoutrefresh(state->toolbar);
340 static void setup_mainwin(struct app_state *state)
342 int w, h, gamesz, goalsz, scr_w, scr_h, split;
344 getmaxyx(stdscr, scr_h, scr_w);
346 /* First try to fit the game tiles based on window height. */
347 gamesz = MAX(3, (scr_h - 4) / 5);
349 /* Adjust downward until we can fit smallest possible goal area. */
350 for (; split = 5+10*gamesz, gamesz > 3; gamesz--) {
351 if (split + 20 < scr_w)
355 /* Pick a goal size that will fit in the remaining area */
356 goalsz = MAX(3, (scr_w - split - 4) / 6);
357 if (goalsz >= gamesz)
358 goalsz = MAX(3, gamesz - 1);
360 realloc_tiles(state->gamewin, gamesz);
361 realloc_tiles(state->goalwin, goalsz);
363 /* Frame for game area */
364 w = MIN(scr_w-2, 3+10*gamesz);
365 h = MIN(scr_h-GAME_YPOS, 2+5*gamesz);
366 realloc_area(&state->gamewin[WINDOW_AREA], h, w, GAME_YPOS, 2);
368 /* Frame for goal area */
369 w = MIN(scr_w-split, 3+6*goalsz);
370 h = MIN(scr_h-GAME_YPOS, 2+3*goalsz);
371 realloc_area(&state->goalwin[WINDOW_AREA], h, w, GAME_YPOS, split);
374 w = MAX(0, scr_w-split-1);
375 realloc_area(&state->timer, 1, w, GAME_YPOS+h, split+1);
378 realloc_area(&state->toolbar, 1, scr_w, scr_h-1, 0);
382 static void app_initialize(int argc, char **argv)
384 int enable_mouse = 1;
390 while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) {
410 game_reset(&state.board);
418 keypad(stdscr, TRUE);
421 * While we only care about a subset of these events, for
422 * some reason with ncurses failing to enable all of them
423 * causes timeout to stop working when disabled buttons are
424 * pressed (or released).
426 * It is not known if other curses have this problem; at least
427 * pdcurses does not, but the extra events should be harmless
430 #if HAVE_CURSES_MOUSE_SUPPORT
431 unsigned long mask = BUTTON1_PRESSED | BUTTON1_RELEASED
432 | BUTTON2_PRESSED | BUTTON2_RELEASED
433 | BUTTON3_PRESSED | BUTTON3_RELEASED
434 #ifdef BUTTON4_PRESSED
435 | BUTTON4_PRESSED | BUTTON4_RELEASED
437 #ifdef BUTTON5_PRESSED
438 | BUTTON5_PRESSED | BUTTON5_RELEASED
441 #if HAVE_CURSES_MOUSE_SET
443 #elif HAVE_CURSES_MOUSEMASK
444 mousemask(mask, NULL);
446 #if HAVE_CURSES_MOUSEINTERVAL
449 #endif /* HAVE_CURSES_MOUSE_SUPPORT */
454 init_pair(TILE_RED, COLOR_RED, COLOR_BLACK);
455 init_pair(TILE_ORANGE, COLOR_YELLOW, COLOR_BLACK);
456 init_pair(TILE_YELLOW, COLOR_YELLOW, COLOR_BLACK);
457 init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
458 init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
459 init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
460 init_pair(RR_COLOUR_CURSOR, COLOR_BLACK, COLOR_BLACK);
461 init_pair(RR_COLOUR_TOOLBAR, COLOR_CYAN, COLOR_BLACK);
463 setup_mainwin(&state);
467 static void update_timer(struct app_state *state, uint_fast32_t ms)
471 state->timer_ms = ms;
472 sec = ms / 1000; ms %= 1000;
473 min = sec / 60; sec %= 60;
474 mvwprintw(state->timer, 0, 0, "Time: %u:%.2u.%.3u",
475 min, sec, (unsigned)ms);
476 wclrtoeol(state->timer);
477 wrefresh(state->timer);
480 static uint_fast32_t do_move(struct app_state *state, int x, int y)
484 if ((mask = game_do_move(&state->board, x, y)) != 0) {
485 uint_fast32_t goal = game_check_goal(&state->board);
487 if (state->view_goal_on_game) {
488 state->view_goal_on_game = 0;
494 update_timer(state, game_finish(&state->board));
500 curs_redraw_game(state, mask);
507 static void do_reset_cursor(struct app_state *state)
509 state->cursor = 5*state->board.y + state->board.x;
512 static void do_new_game(struct app_state *state)
514 game_reset(&state->board);
516 do_reset_cursor(state);
517 curs_redraw_game(state, -1);
518 curs_redraw_goal(state, -1);
519 update_timer(state, 0);
523 game_begin(&state->board);
526 static void do_function(struct app_state *state, unsigned func)
538 #if HAVE_CURSES_MOUSE_SUPPORT
540 * Returns the toolbar function (1 through 10) under the given x, y screen
541 * coordinates, or 0 if the coordinates are outside of the toolbar.
543 static int mouse_toolbar_function(struct app_state *state, int x, int y)
545 int toolbar_x, toolbar_y, toolbar_w, toolbar_h, i;
547 getbegyx(state->toolbar, toolbar_y, toolbar_x);
548 getmaxyx(state->toolbar, toolbar_h, toolbar_w);
550 if ((void)toolbar_x, y != toolbar_y)
553 /* OK, selected a button, determine which one */
554 for (i = 10; i > 1; i--) {
555 if ((void)toolbar_h, x >= toolbar_xpos(i, toolbar_w))
563 * Given x, y as screen coordinates, record which (if any) toolbar function
564 * label is at that position, to be performed later.
566 * If no function is indicated, returns zero. Otherwise, returns non-zero.
568 static int press_toolbar(struct app_state *state, int x, int y)
570 return state->toolbar_click = mouse_toolbar_function(state, x, y);
574 * Perform the action previously recorded by press_toolbar, if and only if
575 * the x, y screen coordinates correspond to the same function.
577 static void release_toolbar(struct app_state *state, int x, int y)
579 int func = mouse_toolbar_function(state, x, y);
581 if (func && state->toolbar_click == func) {
582 do_function(state, func);
585 state->toolbar_click = 0;
589 * Given x, y as screen coordinates, determine which (if any) game tile is
592 * If there is no such tile, performs no action and returns 0.
594 * Otherwise, attempts to move that tile and returns non-zero.
596 static int press_tile(struct app_state *state, int x, int y)
598 int game_x, game_y, tile_w, tile_h;
599 uint_fast32_t cursor_mask, move_mask;
601 getbegyx(state->gamewin[WINDOW_AREA], game_y, game_x);
602 getmaxyx(state->gamewin[WINDOW_TILEBORDER], tile_h, tile_w);
603 tile_w += tile_w & 1;
605 /* special case the left spacer column */
609 if (x < game_x+2 || (x -= game_x+2)/5 >= tile_w)
611 if (y < game_y+1 || (y -= game_y+1)/5 >= tile_h)
614 /* OK, selected a tile. */
618 /* Disable the keyboard cursor due to mouse action */
619 cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor;
622 move_mask = do_move(state, x, y);
623 if ((cursor_mask & move_mask) == 0) {
624 curs_redraw_game(state, cursor_mask);
631 static void do_mouse(struct app_state *state)
633 unsigned long bstate;
636 #if HAVE_CURSES_GETMOUSE_NCURSES
639 if (getmouse(&mev) == ERR)
642 x = mev.x, y = mev.y;
644 #elif HAVE_CURSES_REQUEST_MOUSE_POS
651 #define set_bstate_helper(button) do { if (BUTTON_CHANGED(button)) \
652 switch (BUTTON_STATUS(button)) { \
653 case BUTTON_RELEASED: bstate |= BUTTON ## button ## _RELEASED; break; \
654 case BUTTON_PRESSED: bstate |= BUTTON ## button ## _PRESSED; break; \
658 set_bstate_helper(1);
659 set_bstate_helper(3);
661 if (bstate & BUTTON3_PRESSED) {
662 state->view_goal_on_game = 1;
663 curs_redraw_game(state, game_check_goal(&state->board));
667 if (bstate & BUTTON3_RELEASED) {
668 state->view_goal_on_game = 0;
669 curs_redraw_game(state, game_check_goal(&state->board));
673 /* Ignore button1 if holding button3 to view goal */
674 if (state->view_goal_on_game & 1)
677 if (bstate & BUTTON1_PRESSED) {
678 if (press_toolbar(state, x, y));
679 else if (press_tile(state, x, y));
682 if (bstate & BUTTON1_RELEASED) {
683 release_toolbar(state, x, y);
686 #endif /* HAVE_CURSES_MOUSE_SUPPORT */
688 static void do_move_cursor(struct app_state *state, int c)
690 uint_fast32_t mask = 0;
692 if (state->cursor < 0) {
693 /* Cursor was hidden; reset it */
694 do_reset_cursor(state);
696 mask = 1ul << state->cursor;
699 if (state->view_goal_on_game) {
700 state->view_goal_on_game = 0;
701 mask |= game_check_goal(&state->board);
706 if ((state->cursor -= 5) < 0)
710 if ((state->cursor += 5) >= 25)
714 if ((state->cursor -= 1) % 5 == 4 || state->cursor < 0)
718 if ((state->cursor += 1) % 5 == 0)
723 curs_redraw_game(state, mask | 1ul << state->cursor);
727 static void do_keystroke(struct app_state *state, int c)
729 int last_input = state->last_input;
730 state->last_input = c;
733 case KEY_DOWN: case KEY_UP: case KEY_LEFT: case KEY_RIGHT:
734 do_move_cursor(state, c);
737 state->view_goal_on_game ^= 2u;
738 curs_redraw_game(state, game_check_goal(&state->board));
742 if (!(state->view_goal_on_game & 1) && state->cursor >= 0)
743 do_move(state, state->cursor%5, state->cursor/5);
748 if (last_input == '\33' && c >= '0' && c <= '9') {
749 do_function(state, (c -= '0') == 0 ? 10 : c);
753 if (c >= KEY_F(1) && c <= KEY_F(10)) {
754 do_function(state, c - KEY_F0);
758 /* One iteration of main input loop */
759 void do_mainloop(struct app_state *state)
766 setup_mainwin(state);
769 curs_redraw_game(state, -1);
770 curs_redraw_goal(state, -1);
772 update_timer(state, state->timer_ms);
775 #if HAVE_CURSES_MOUSE_SUPPORT
781 do_keystroke(state, c);
785 if (state->board.x <= 4)
786 update_timer(state, game_elapsed(&state->board));
789 int main(int argc, char **argv)
791 setlocale(LC_ALL, "");
792 app_initialize(argc, argv);