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/>.
30 #include "cursesopt.h"
33 GAME_YPOS = 1 // top row of game and goal areas.
36 static const char *progname = "rrace";
37 static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
39 static struct app_state state;
41 static void print_version(void)
43 version_print_head("rrace-curses", stdout);
44 puts("License GPLv3+: GNU GPL version 3 or any later version");
45 puts("This is free software: you are free to change and redistribute it.");
46 puts("There is NO WARRANTY, to the extent permitted by law.");
49 static void print_usage(FILE *f)
51 fprintf(f, "Usage: %s [options]\n", progname);
53 fprintf(f, "Try %s --help for more information.\n", progname);
56 static void print_help(void)
58 struct lopt_help help = {0};
59 const struct option *opt;
65 for (opt = lopts; opt->name; opt++) {
66 if (!lopt_get_help(opt, &help))
68 help_print_option(opt, help.arg, help.desc, 20);
72 printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
75 static void draw_tile(WINDOW **win, unsigned colour, unsigned selected,
76 unsigned x, unsigned y, unsigned start_column)
78 WINDOW *border = win[PLAYWIN_TILEBORDER], *fill = win[PLAYWIN_TILEFILL];
79 int w, h, attr, ch, bc = selected ? '#' : 0;
81 assert(colour < TILE_MAX);
82 attr = COLOR_PAIR(colour);
84 case TILE_RED: ch = 'X'; break;
85 case TILE_ORANGE: ch = '|'; break;
86 case TILE_GREEN: ch = '+'; break;
87 case TILE_YELLOW: ch = '~'; attr |= A_BOLD; break;
88 case TILE_BLUE: ch = 'o'; attr |= A_BOLD; break;
89 case TILE_WHITE: ch = '.'; attr |= A_BOLD; break;
91 case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(RR_COLOUR_SHADOW);
94 getmaxyx(border, h, w);
97 if (mvwin(border, 1+GAME_YPOS+h*y, start_column+w*x) == ERR)
100 if (colour != TILE_EMPTY) {
101 mvderwin(fill, 1, 1);
102 wbkgdset(fill, A_REVERSE|attr|ch);
109 wattrset(border, attr);
110 wborder(border, bc, bc, bc, bc, bc, bc, bc, bc);
113 wnoutrefresh(border);
117 redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column,
118 uint_least32_t *gp, unsigned selected)
120 unsigned tile = board_tile(gp, 5*y+x);
122 assert(tile < TILE_MAX);
123 draw_tile(win, tile, selected, x, y, start_column);
127 static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
129 int w, h, tr = 0, rs = 0, br = 0, bs = 0, bl = 0;
130 WINDOW *area = win[PLAYWIN_AREA];
132 getmaxyx(stdscr, h, w);
134 if (h <= GAME_YPOS+1) {
135 bl = ACS_ULCORNER, br = ACS_URCORNER;
136 } else if (h <= 3*sz+GAME_YPOS+1) {
137 bl = br = ACS_VLINE, bs = ' ';
141 tr = ACS_ULCORNER, br = ACS_LLCORNER;
142 } else if (w <= 6*sz+x+2) {
143 tr = ACS_HLINE, rs = ' ';
144 br = br ? ' ' : ACS_HLINE;
147 wborder(area, 0, rs, 0, bs, 0, tr, bl, br);
151 static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
153 uint_least32_t buf[3], *gp = state->board.game;
157 redraw_area_border(state->gamewin, 2, 5);
159 if (state->view_goal_on_game) {
160 for (i = 0; i < 3; i++) {
161 buf[i] = state->board.goal[i];
162 buf[i] = (gp[i] & ~GOAL_MASK) | (buf[i] << GOAL_SHIFT);
167 for (i = 0; i < 25; i++) {
169 redraw_tile(state->gamewin, i%5, i/5,
170 4, gp, i == state->cursor);
176 static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask)
178 uint_least32_t gp[3] = {
179 state->board.goal[0],
180 state->board.goal[1],
185 if (!state->goalwin[PLAYWIN_AREA])
188 getbegyx(state->goalwin[PLAYWIN_AREA], y, x);
190 redraw_area_border(state->goalwin, x, 3);
192 for (i = 0; i < 9; i++) {
194 redraw_tile(state->goalwin, i%3, i/3, x+2, gp, 0);
200 /* Thin wrapper around wresize which returns ERR if support is unavailable. */
201 static int do_wresize(WINDOW *window, int h, int w)
203 #if HAVE_CURSES_WRESIZE
204 return wresize(window, h, w);
209 static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x)
214 if (do_wresize(win, h, w) != ERR) {
223 return *orig = subwin(stdscr, h, w, y, x);
227 static void realloc_tiles(WINDOW **win, int h)
229 WINDOW *border = win[PLAYWIN_TILEBORDER], *fill = win[PLAYWIN_TILEFILL];
232 if (fill && border) {
235 if (do_wresize(fill, h-2, w-2) != ERR
236 && do_wresize(border, h, w) != ERR)
239 getmaxyx(border, old_h, old_w);
249 win[PLAYWIN_TILEBORDER] = border = newwin(h, w, 0, 0);
250 win[PLAYWIN_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
253 static void setup_mainwin(struct app_state *state)
255 int w, h, gamesz, goalsz, scr_w, scr_h, split;
257 getmaxyx(stdscr, scr_h, scr_w);
259 /* First try to fit the game tiles based on window height. */
260 gamesz = MAX(3, (scr_h - 4) / 5);
262 /* Adjust downward until we can fit smallest possible goal area. */
263 for (; split = 5+10*gamesz, gamesz > 3; gamesz--) {
264 if (split + 20 < scr_w)
268 /* Pick a goal size that will fit in the remaining area */
269 goalsz = MAX(3, (scr_w - split - 4) / 6);
270 if (goalsz >= gamesz)
271 goalsz = MAX(3, gamesz - 1);
273 realloc_tiles(state->gamewin, gamesz);
274 realloc_tiles(state->goalwin, goalsz);
276 /* Frame for game area */
277 w = MIN(scr_w-2, 3+10*gamesz);
278 h = MIN(scr_h-GAME_YPOS, 2+5*gamesz);
279 realloc_area(&state->gamewin[PLAYWIN_AREA], h, w, GAME_YPOS, 2);
281 /* Frame for goal area */
282 w = MIN(scr_w-split, 3+6*goalsz);
283 h = MIN(scr_h-GAME_YPOS, 2+3*goalsz);
284 realloc_area(&state->goalwin[PLAYWIN_AREA], h, w, GAME_YPOS, split);
287 w = MAX(0, scr_w-split-1);
288 realloc_area(&state->timer, 1, w, GAME_YPOS+h, split+1);
291 realloc_area(&state->toolbar, 1, scr_w, scr_h-1, 0);
292 curs_draw_toolbar(state);
295 static void app_initialize(int argc, char **argv)
297 int enable_mouse = 1;
303 while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) {
323 game_reset(&state.board);
331 keypad(stdscr, TRUE);
334 * While we only care about a subset of these events, for
335 * some reason with ncurses failing to enable all of them
336 * causes timeout to stop working when disabled buttons are
337 * pressed (or released).
339 * It is not known if other curses have this problem; at least
340 * pdcurses does not, but the extra events should be harmless
343 #if HAVE_CURSES_MOUSE_SUPPORT
344 unsigned long mask = BUTTON1_PRESSED | BUTTON1_RELEASED
345 | BUTTON2_PRESSED | BUTTON2_RELEASED
346 | BUTTON3_PRESSED | BUTTON3_RELEASED
347 #ifdef BUTTON4_PRESSED
348 | BUTTON4_PRESSED | BUTTON4_RELEASED
350 #ifdef BUTTON5_PRESSED
351 | BUTTON5_PRESSED | BUTTON5_RELEASED
354 #if HAVE_CURSES_MOUSE_SET
356 #elif HAVE_CURSES_MOUSEMASK
357 mousemask(mask, NULL);
359 #if HAVE_CURSES_MOUSEINTERVAL
362 #endif /* HAVE_CURSES_MOUSE_SUPPORT */
367 init_pair(TILE_RED, COLOR_RED, COLOR_BLACK);
368 init_pair(TILE_ORANGE, COLOR_YELLOW, COLOR_BLACK);
369 init_pair(TILE_YELLOW, COLOR_YELLOW, COLOR_BLACK);
370 init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
371 init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
372 init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
373 init_pair(RR_COLOUR_SHADOW, COLOR_BLACK, COLOR_BLACK);
374 init_pair(RR_COLOUR_TOOLBAR, COLOR_CYAN, COLOR_BLACK);
376 setup_mainwin(&state);
380 static void update_timer(struct app_state *state, uint_fast32_t ms)
384 state->timer_ms = ms;
385 sec = ms / 1000; ms %= 1000;
386 min = sec / 60; sec %= 60;
387 mvwprintw(state->timer, 0, 0, "Time: %u:%.2u.%.3u",
388 min, sec, (unsigned)ms);
389 wclrtoeol(state->timer);
390 wnoutrefresh(state->timer);
393 static uint_fast32_t do_move(struct app_state *state, int x, int y)
397 if ((mask = game_do_move(&state->board, x, y)) != 0) {
398 uint_fast32_t goal = game_check_goal(&state->board);
400 if (state->view_goal_on_game) {
401 state->view_goal_on_game = 0;
407 update_timer(state, game_finish(&state->board));
413 curs_redraw_game(state, mask);
420 static void do_reset_cursor(struct app_state *state)
422 state->cursor = 5*state->board.y + state->board.x;
425 void curs_new_game(struct app_state *state)
427 game_reset(&state->board);
429 do_reset_cursor(state);
430 curs_redraw_game(state, -1);
431 curs_redraw_goal(state, -1);
432 update_timer(state, 0);
436 game_begin(&state->board);
439 #if HAVE_CURSES_MOUSE_SUPPORT
441 * Given x, y as screen coordinates, record which (if any) toolbar function
442 * label is at that position, to be performed later.
444 * If no function is indicated, returns zero. Otherwise, returns non-zero.
446 static int press_toolbar(struct app_state *state, int x, int y)
448 return state->toolbar_click = curs_toolbar_mouse_func(state, x, y);
452 * Given x, y as screen coordinates, perform the toolbar action.
454 * Perform the action previously recorded by press_toolbar, if and only if
455 * the x, y screen coordinates correspond to the same function.
457 static void release_toolbar(struct app_state *state, int x, int y)
459 int func = curs_toolbar_mouse_func(state, x, y);
461 if (func && state->toolbar_click == func) {
462 curs_execute_function(state, func);
465 state->toolbar_click = 0;
469 * Given x, y as screen coordinates, determine which (if any) game tile is
472 * If there is no such tile, performs no action and returns 0.
474 * Otherwise, attempts to move that tile and returns non-zero.
476 static int press_tile(struct app_state *state, int x, int y)
478 int game_x, game_y, tile_w, tile_h;
479 uint_fast32_t cursor_mask, move_mask;
481 getbegyx(state->gamewin[PLAYWIN_AREA], game_y, game_x);
482 getmaxyx(state->gamewin[PLAYWIN_TILEBORDER], tile_h, tile_w);
483 tile_w += tile_w & 1;
485 /* special case the left spacer column */
489 if (x < game_x+2 || (x -= game_x+2)/5 >= tile_w)
491 if (y < game_y+1 || (y -= game_y+1)/5 >= tile_h)
494 /* OK, selected a tile. */
498 /* Disable the keyboard cursor due to mouse action */
499 cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor;
502 move_mask = do_move(state, x, y);
503 if ((cursor_mask & move_mask) == 0) {
504 curs_redraw_game(state, cursor_mask);
511 static void do_mouse(struct app_state *state)
513 unsigned long bstate;
516 #if HAVE_CURSES_GETMOUSE_NCURSES
519 if (getmouse(&mev) == ERR)
522 x = mev.x, y = mev.y;
524 #elif HAVE_CURSES_REQUEST_MOUSE_POS
531 #define set_bstate_helper(button) do { if (BUTTON_CHANGED(button)) \
532 switch (BUTTON_STATUS(button)) { \
533 case BUTTON_RELEASED: bstate |= BUTTON ## button ## _RELEASED; break; \
534 case BUTTON_PRESSED: bstate |= BUTTON ## button ## _PRESSED; break; \
538 set_bstate_helper(1);
539 set_bstate_helper(3);
541 if (bstate & BUTTON3_PRESSED) {
542 state->view_goal_on_game = 1;
543 curs_redraw_game(state, game_check_goal(&state->board));
547 if (bstate & BUTTON3_RELEASED) {
548 state->view_goal_on_game = 0;
549 curs_redraw_game(state, game_check_goal(&state->board));
553 /* Ignore button1 if holding button3 to view goal */
554 if (state->view_goal_on_game & 1)
557 if (bstate & BUTTON1_PRESSED) {
558 if (press_toolbar(state, x, y));
559 else if (press_tile(state, x, y));
562 if (bstate & BUTTON1_RELEASED) {
563 release_toolbar(state, x, y);
566 #endif /* HAVE_CURSES_MOUSE_SUPPORT */
568 static void do_move_cursor(struct app_state *state, int c)
570 uint_fast32_t mask = 0;
572 if (state->cursor < 0) {
573 /* Cursor was hidden; reset it */
574 do_reset_cursor(state);
576 mask = 1ul << state->cursor;
579 if (state->view_goal_on_game) {
580 state->view_goal_on_game = 0;
581 mask |= game_check_goal(&state->board);
586 if ((state->cursor -= 5) < 0)
590 if ((state->cursor += 5) >= 25)
594 if ((state->cursor -= 1) % 5 == 4 || state->cursor < 0)
598 if ((state->cursor += 1) % 5 == 0)
603 curs_redraw_game(state, mask | 1ul << state->cursor);
607 static void do_keystroke(struct app_state *state, int c)
609 int last_input = state->last_input;
610 state->last_input = c;
613 case KEY_DOWN: case KEY_UP: case KEY_LEFT: case KEY_RIGHT:
614 do_move_cursor(state, c);
617 state->view_goal_on_game ^= 2u;
618 curs_redraw_game(state, game_check_goal(&state->board));
622 if (!(state->view_goal_on_game & 1) && state->cursor >= 0)
623 do_move(state, state->cursor%5, state->cursor/5);
628 if (last_input == '\33' && c >= '0' && c <= '9') {
629 curs_execute_function(state, (c -= '0') == 0 ? 10 : c);
633 if (c >= KEY_F(1) && c <= KEY_F(10)) {
634 curs_execute_function(state, c - KEY_F0);
638 /* One iteration of main input loop */
639 void do_mainloop(struct app_state *state)
646 setup_mainwin(state);
649 curs_redraw_game(state, -1);
650 curs_redraw_goal(state, -1);
651 curs_draw_toolbar(state);
652 update_timer(state, state->timer_ms);
656 #if HAVE_CURSES_MOUSE_SUPPORT
662 do_keystroke(state, c);
666 if (state->board.x <= 4) {
667 update_timer(state, game_elapsed(&state->board));
672 int main(int argc, char **argv)
674 setlocale(LC_ALL, "");
675 app_initialize(argc, argv);
677 curs_new_game(&state);