]> git.draconx.ca Git - rrace.git/blob - src/curses.c
Factor out build-conditional wresize calls.
[rrace.git] / src / curses.c
1 /*
2  * Curses UI for slide puzzle game
3  * Copyright © 2022 Nick Bowler
4  *
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.
9  *
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.
14  *
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/>.
17  */
18
19 #include <config.h>
20 #include <stdlib.h>
21 #include <locale.h>
22 #include <assert.h>
23 #include <getopt.h>
24 #include <curses.h>
25
26 #include "help.h"
27 #include "version.h"
28
29 #include "cursesui.h"
30 #include "cursesopt.h"
31
32 enum {
33         GAME_YPOS = 1 // top row of game and goal areas.
34 };
35
36 static const char *progname = "rrace";
37 static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
38
39 static struct app_state state;
40
41 static void print_version(void)
42 {
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.");
47 }
48
49 static void print_usage(FILE *f)
50 {
51         fprintf(f, "Usage: %s [options]\n", progname);
52         if (f != stdout)
53                 fprintf(f, "Try %s --help for more information.\n", progname);
54 }
55
56 static void print_help(void)
57 {
58         struct lopt_help help = {0};
59         const struct option *opt;
60
61         print_usage(stdout);
62
63         putchar('\n');
64         puts("Options:");
65         for (opt = lopts; opt->name; opt++) {
66                 if (!lopt_get_help(opt, &help))
67                         continue;
68                 help_print_option(opt, help.arg, help.desc, 20);
69         }
70         putchar('\n');
71
72         printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
73 }
74
75 static void draw_tile(WINDOW **win, unsigned colour, unsigned selected,
76                       unsigned x, unsigned y, unsigned start_column)
77 {
78         WINDOW *border = win[PLAYWIN_TILEBORDER], *fill = win[PLAYWIN_TILEFILL];
79         int w, h, attr, ch, bc = selected ? '#' : 0;
80
81         assert(colour < TILE_MAX);
82         attr = COLOR_PAIR(colour);
83         switch (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;
90
91         case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(RR_COLOUR_SHADOW);
92         }
93
94         getmaxyx(border, h, w);
95         w = 2*(w+1)/2;
96
97         if (mvwin(border, 1+GAME_YPOS+h*y, start_column+w*x) == ERR)
98                 return;
99
100         if (colour != TILE_EMPTY) {
101                 mvderwin(fill, 1, 1);
102                 wbkgdset(fill, A_REVERSE|attr|ch);
103                 werase(fill);
104         } else {
105                 werase(border);
106         }
107
108         if (bc || colour) {
109                 wattrset(border, attr);
110                 wborder(border, bc, bc, bc, bc, bc, bc, bc, bc);
111         }
112
113         wnoutrefresh(border);
114 }
115
116 static int
117 redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column,
118             uint_fast32_t bit0, uint_fast32_t bit1, uint_fast32_t bit2,
119             unsigned selected)
120 {
121         uint_fast32_t pos = board_position(x, y);
122         unsigned char tile = 0;
123
124         if (bit0 & pos) tile |= 1;
125         if (bit1 & pos) tile |= 2;
126         if (bit2 & pos) tile |= 4;
127         assert(tile < TILE_MAX);
128
129         draw_tile(win, tile, selected, x, y, start_column);
130         return tile;
131 }
132
133 static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
134 {
135         int w, h, tr = 0, rs = 0, br = 0, bs = 0, bl = 0;
136         WINDOW *area = win[PLAYWIN_AREA];
137
138         getmaxyx(stdscr, h, w);
139
140         if (h <= GAME_YPOS+1) {
141                 bl = ACS_ULCORNER, br = ACS_URCORNER;
142         } else if (h <= 3*sz+GAME_YPOS+1) {
143                 bl = br = ACS_VLINE, bs = ' ';
144         }
145
146         if (w <= x+1) {
147                 tr = ACS_ULCORNER, br = ACS_LLCORNER;
148         } else if (w <= 6*sz+x+2) {
149                 tr = ACS_HLINE, rs = ' ';
150                 br = br ? ' ' : ACS_HLINE;
151         }
152
153         wborder(area, 0, rs, 0, bs, 0, tr, bl, br);
154         wnoutrefresh(area);
155 }
156
157 static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
158 {
159         uint_least32_t buf[3], *gp = state->board.game;
160         int i;
161
162         if (mask == -1)
163                 redraw_area_border(state->gamewin, 2, 5);
164
165         if (state->view_goal_on_game) {
166                 for (i = 0; i < 3; i++) {
167                         buf[i] = state->board.goal[i];
168                         buf[i] = (gp[i] & ~GOAL_MASK) | (buf[i] << GOAL_SHIFT);
169                 }
170                 gp = buf;
171         }
172
173         for (i = 0; i < 25; i++) {
174                 if (mask & 1) {
175                         redraw_tile(state->gamewin, i%5, i/5,
176                                     4, gp[0], gp[1], gp[2],
177                                     i == state->cursor);
178                 }
179                 mask >>= 1;
180         }
181 }
182
183 static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask)
184 {
185         uint_least16_t *gp = state->board.goal;
186         int i, x, y;
187
188         if (!state->goalwin[PLAYWIN_AREA])
189                 return;
190
191         getbegyx(state->goalwin[PLAYWIN_AREA], y, x);
192         if (mask == -1)
193                 redraw_area_border(state->goalwin, x, 3);
194
195         for (i = 0; i < 9; i++) {
196                 if (mask & 1) {
197                         redraw_tile(state->goalwin, i%3, i/3,
198                                     x+2, gp[0], gp[1], gp[2], 0);
199                 }
200                 mask >>= 1;
201         }
202 }
203
204 /* Thin wrapper around wresize which returns ERR if support is unavailable. */
205 static int do_wresize(WINDOW *window, int lines, int columns)
206 {
207 #if HAVE_CURSES_WRESIZE
208         return wresize(window, lines, columns);
209 #endif
210         return ERR;
211 }
212
213 static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x)
214 {
215         WINDOW *win = *orig;
216
217         if (win) {
218                 if (do_wresize(win, h, w) != ERR) {
219                         mvwin(win, y, x);
220                         return win;
221                 }
222
223                 delwin(win);
224         }
225
226         if (w > 0 && h > 0)
227                 return *orig = subwin(stdscr, h, w, y, x);
228         return *orig = NULL;
229 }
230
231 static void realloc_tiles(WINDOW **win, int h)
232 {
233         WINDOW *border = win[PLAYWIN_TILEBORDER], *fill = win[PLAYWIN_TILEFILL];
234         int w = 2*h - 1;
235
236         if (fill && border) {
237                 int old_w, old_h;
238
239                 if (do_wresize(fill, h-2, w-2) != ERR
240                     && do_wresize(border, h, w) != ERR)
241                         return;
242
243                 getmaxyx(border, old_h, old_w);
244                 if (old_h == h)
245                         return;
246         }
247
248         if (fill)
249                 delwin(fill);
250         if (border)
251                 delwin(border);
252
253         win[PLAYWIN_TILEBORDER] = border = newwin(h, w, 0, 0);
254         win[PLAYWIN_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
255 }
256
257 static void setup_mainwin(struct app_state *state)
258 {
259         int w, h, gamesz, goalsz, scr_w, scr_h, split;
260
261         getmaxyx(stdscr, scr_h, scr_w);
262
263         /* First try to fit the game tiles based on window height. */
264         gamesz = MAX(3, (scr_h - 4) / 5);
265
266         /* Adjust downward until we can fit smallest possible goal area. */
267         for (; split = 5+10*gamesz, gamesz > 3; gamesz--) {
268                 if (split + 20 < scr_w)
269                         break;
270         }
271
272         /* Pick a goal size that will fit in the remaining area */
273         goalsz = MAX(3, (scr_w - split - 4) / 6);
274         if (goalsz >= gamesz)
275                 goalsz = MAX(3, gamesz - 1);
276
277         realloc_tiles(state->gamewin, gamesz);
278         realloc_tiles(state->goalwin, goalsz);
279
280         /* Frame for game area */
281         w = MIN(scr_w-2, 3+10*gamesz);
282         h = MIN(scr_h-GAME_YPOS, 2+5*gamesz);
283         realloc_area(&state->gamewin[PLAYWIN_AREA], h, w, GAME_YPOS, 2);
284
285         /* Frame for goal area */
286         w = MIN(scr_w-split, 3+6*goalsz);
287         h = MIN(scr_h-GAME_YPOS, 2+3*goalsz);
288         realloc_area(&state->goalwin[PLAYWIN_AREA], h, w, GAME_YPOS, split);
289
290         /* Status area */
291         w = MAX(0, scr_w-split-1);
292         realloc_area(&state->timer, 1, w, GAME_YPOS+h, split+1);
293
294         /* Toolbar */
295         realloc_area(&state->toolbar, 1, scr_w, scr_h-1, 0);
296         curs_draw_toolbar(state);
297 }
298
299 static void app_initialize(int argc, char **argv)
300 {
301         int enable_mouse = 1;
302         int opt;
303
304         if (argc > 0)
305                 progname = argv[0];
306
307         while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) {
308                 switch (opt) {
309                 case LOPT_MOUSE:
310                         enable_mouse = 2;
311                         break;
312                 case LOPT_NO_MOUSE:
313                         enable_mouse = 0;
314                         break;
315                 case LOPT_VERSION:
316                         print_version();
317                         exit(EXIT_SUCCESS);
318                 case LOPT_HELP:
319                         print_help();
320                         exit(EXIT_SUCCESS);
321                 default:
322                         print_usage(stderr);
323                         exit(EXIT_FAILURE);
324                 }
325         }
326
327         game_reset(&state.board);
328         state.cursor = -1;
329
330         initscr();
331         start_color();
332         curs_set(0);
333
334         cbreak();
335         keypad(stdscr, TRUE);
336         if (enable_mouse) {
337                 /*
338                  * While we only care about a subset of these events, for
339                  * some reason with ncurses failing to enable all of them
340                  * causes timeout to stop working when disabled buttons are
341                  * pressed (or released).
342                  *
343                  * It is not known if other curses have this problem; at least
344                  * pdcurses does not, but the extra events should be harmless
345                  * in any case.
346                  */
347 #if HAVE_CURSES_MOUSE_SUPPORT
348                 unsigned long mask = BUTTON1_PRESSED | BUTTON1_RELEASED
349                                    | BUTTON2_PRESSED | BUTTON2_RELEASED
350                                    | BUTTON3_PRESSED | BUTTON3_RELEASED
351 #ifdef BUTTON4_PRESSED
352                                    | BUTTON4_PRESSED | BUTTON4_RELEASED
353 #endif
354 #ifdef BUTTON5_PRESSED
355                                    | BUTTON5_PRESSED | BUTTON5_RELEASED
356 #endif
357                                    ;
358 #if HAVE_CURSES_MOUSE_SET
359                 mouse_set(mask);
360 #elif HAVE_CURSES_MOUSEMASK
361                 mousemask(mask, NULL);
362 #endif
363 #if HAVE_CURSES_MOUSEINTERVAL
364                 mouseinterval(0);
365 #endif
366 #endif /* HAVE_CURSES_MOUSE_SUPPORT */
367         }
368
369         noecho();
370
371         init_pair(TILE_RED, COLOR_RED, COLOR_BLACK);
372         init_pair(TILE_ORANGE, COLOR_YELLOW, COLOR_BLACK);
373         init_pair(TILE_YELLOW, COLOR_YELLOW, COLOR_BLACK);
374         init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
375         init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
376         init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
377         init_pair(RR_COLOUR_SHADOW, COLOR_BLACK, COLOR_BLACK);
378         init_pair(RR_COLOUR_TOOLBAR, COLOR_CYAN, COLOR_BLACK);
379
380         setup_mainwin(&state);
381         refresh();
382 }
383
384 static void update_timer(struct app_state *state, uint_fast32_t ms)
385 {
386         unsigned sec, min;
387
388         state->timer_ms = ms;
389         sec = ms / 1000; ms %= 1000;
390         min = sec / 60; sec %= 60;
391         mvwprintw(state->timer, 0, 0, "Time: %u:%.2u.%.3u",
392                   min, sec, (unsigned)ms);
393         wclrtoeol(state->timer);
394         wnoutrefresh(state->timer);
395 }
396
397 static uint_fast32_t do_move(struct app_state *state, int x, int y)
398 {
399         uint_fast32_t mask;
400
401         if ((mask = game_do_move(&state->board, x, y)) != 0) {
402                 uint_fast32_t goal = game_check_goal(&state->board);
403
404                 if (state->view_goal_on_game) {
405                         state->view_goal_on_game = 0;
406                         mask |= goal;
407                 }
408
409                 if (goal == 0) {
410                         /* Solved! */
411                         update_timer(state, game_finish(&state->board));
412                         mask |= ~GOAL_MASK;
413                         state->cursor = -1;
414                         timeout(-1);
415                 }
416
417                 curs_redraw_game(state, mask);
418                 doupdate();
419         }
420
421         return mask;
422 }
423
424 static void do_reset_cursor(struct app_state *state)
425 {
426         state->cursor = 5*state->board.y + state->board.x;
427 }
428
429 void curs_new_game(struct app_state *state)
430 {
431         game_reset(&state->board);
432
433         do_reset_cursor(state);
434         curs_redraw_game(state, -1);
435         curs_redraw_goal(state, -1);
436         update_timer(state, 0);
437         doupdate();
438
439         timeout(33);
440         game_begin(&state->board);
441 }
442
443 #if HAVE_CURSES_MOUSE_SUPPORT
444 /*
445  * Given x, y as screen coordinates, record which (if any) toolbar function
446  * label is at that position, to be performed later.
447  *
448  * If no function is indicated, returns zero.  Otherwise, returns non-zero.
449  */
450 static int press_toolbar(struct app_state *state, int x, int y)
451 {
452         return state->toolbar_click = curs_toolbar_mouse_func(state, x, y);
453 }
454
455 /*
456  * Given x, y as screen coordinates, perform the toolbar action.
457  *
458  * Perform the action previously recorded by press_toolbar, if and only if
459  * the x, y screen coordinates correspond to the same function.
460  */
461 static void release_toolbar(struct app_state *state, int x, int y)
462 {
463         int func = curs_toolbar_mouse_func(state, x, y);
464
465         if (func && state->toolbar_click == func) {
466                 curs_execute_function(state, func);
467         }
468
469         state->toolbar_click = 0;
470 }
471
472 /*
473  * Given x, y as screen coordinates, determine which (if any) game tile is
474  * at that position.
475  *
476  * If there is no such tile, performs no action and returns 0.
477  *
478  * Otherwise, attempts to move that tile and returns non-zero.
479  */
480 static int press_tile(struct app_state *state, int x, int y)
481 {
482         int game_x, game_y, tile_w, tile_h;
483         uint_fast32_t cursor_mask, move_mask;
484
485         getbegyx(state->gamewin[PLAYWIN_AREA], game_y, game_x);
486         getmaxyx(state->gamewin[PLAYWIN_TILEBORDER], tile_h, tile_w);
487         tile_w += tile_w & 1;
488
489         /* special case the left spacer column */
490         if (x == game_x+1)
491                 x++;
492
493         if (x < game_x+2 || (x -= game_x+2)/5 >= tile_w)
494                 return 0;
495         if (y < game_y+1 || (y -= game_y+1)/5 >= tile_h)
496                 return 0;
497
498         /* OK, selected a tile. */
499         x /= tile_w;
500         y /= tile_h;
501
502         /* Disable the keyboard cursor due to mouse action */
503         cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor;
504         state->cursor = -1;
505
506         move_mask = do_move(state, x, y);
507         if ((cursor_mask & move_mask) == 0) {
508                 curs_redraw_game(state, cursor_mask);
509                 doupdate();
510         }
511
512         return 1;
513 }
514
515 static void do_mouse(struct app_state *state)
516 {
517         unsigned long bstate;
518         int x, y;
519
520 #if HAVE_CURSES_GETMOUSE_NCURSES
521         MEVENT mev;
522
523         if (getmouse(&mev) == ERR)
524                 return;
525
526         x = mev.x, y = mev.y;
527         bstate = mev.bstate;
528 #elif HAVE_CURSES_REQUEST_MOUSE_POS
529         request_mouse_pos();
530         x = MOUSE_X_POS;
531         y = MOUSE_Y_POS;
532
533         bstate = 0;
534
535 #define set_bstate_helper(button) do { if (BUTTON_CHANGED(button)) \
536         switch (BUTTON_STATUS(button)) { \
537         case BUTTON_RELEASED: bstate |= BUTTON ## button ##  _RELEASED; break; \
538         case BUTTON_PRESSED:  bstate |= BUTTON ## button ##  _PRESSED; break; \
539         } \
540 } while (0);
541
542         set_bstate_helper(1);
543         set_bstate_helper(3);
544 #endif
545         if (bstate & BUTTON3_PRESSED) {
546                 state->view_goal_on_game = 1;
547                 curs_redraw_game(state, game_check_goal(&state->board));
548                 doupdate();
549         }
550
551         if (bstate & BUTTON3_RELEASED) {
552                 state->view_goal_on_game = 0;
553                 curs_redraw_game(state, game_check_goal(&state->board));
554                 doupdate();
555         }
556
557         /* Ignore button1 if holding button3 to view goal */
558         if (state->view_goal_on_game & 1)
559                 return;
560
561         if (bstate & BUTTON1_PRESSED) {
562                 if (press_toolbar(state, x, y));
563                 else if (press_tile(state, x, y));
564         }
565
566         if (bstate & BUTTON1_RELEASED) {
567                 release_toolbar(state, x, y);
568         }
569 }
570 #endif /* HAVE_CURSES_MOUSE_SUPPORT */
571
572 static void do_move_cursor(struct app_state *state, int c)
573 {
574         uint_fast32_t mask = 0;
575
576         if (state->cursor < 0) {
577                 /* Cursor was hidden; reset it */
578                 do_reset_cursor(state);
579         } else {
580                 mask = 1ul << state->cursor;
581         }
582
583         if (state->view_goal_on_game) {
584                 state->view_goal_on_game = 0;
585                 mask |= game_check_goal(&state->board);
586         }
587
588         switch (c) {
589         case KEY_UP:
590                 if ((state->cursor -= 5) < 0)
591                         state->cursor += 25;
592                 break;
593         case KEY_DOWN:
594                 if ((state->cursor += 5) >= 25)
595                         state->cursor -= 25;
596                 break;
597         case KEY_LEFT:
598                 if ((state->cursor -= 1) % 5 == 4 || state->cursor < 0)
599                         state->cursor += 5;
600                 break;
601         case KEY_RIGHT:
602                 if ((state->cursor += 1) % 5 == 0)
603                         state->cursor -= 5;
604                 break;
605         }
606
607         curs_redraw_game(state, mask | 1ul << state->cursor);
608         doupdate();
609 }
610
611 static void do_keystroke(struct app_state *state, int c)
612 {
613         int last_input = state->last_input;
614         state->last_input = c;
615
616         switch (c) {
617         case KEY_DOWN: case KEY_UP: case KEY_LEFT: case KEY_RIGHT:
618                 do_move_cursor(state, c);
619                 break;
620         case '\t':
621                 state->view_goal_on_game ^= 2u;
622                 curs_redraw_game(state, game_check_goal(&state->board));
623                 doupdate();
624                 break;
625         case ' ':
626                 if (!(state->view_goal_on_game & 1) && state->cursor >= 0)
627                         do_move(state, state->cursor%5, state->cursor/5);
628                 break;
629         }
630
631         /* ESC+# keys */
632         if (last_input == '\33' && c >= '0' && c <= '9') {
633                 curs_execute_function(state, (c -= '0') == 0 ? 10 : c);
634         }
635
636         /* F# keys */
637         if (c >= KEY_F(1) && c <= KEY_F(10)) {
638                 curs_execute_function(state, c - KEY_F0);
639         }
640 }
641
642 /* One iteration of main input loop */
643 void do_mainloop(struct app_state *state)
644 {
645         int c = getch();
646
647         switch (c) {
648 #ifdef KEY_RESIZE
649         case KEY_RESIZE:
650                 setup_mainwin(state);
651                 clear();
652                 refresh();
653                 curs_redraw_game(state, -1);
654                 curs_redraw_goal(state, -1);
655                 curs_draw_toolbar(state);
656                 update_timer(state, state->timer_ms);
657                 doupdate();
658                 break;
659 #endif
660 #if HAVE_CURSES_MOUSE_SUPPORT
661         case KEY_MOUSE:
662                 do_mouse(state);
663                 break;
664 #endif
665         default:
666                 do_keystroke(state, c);
667         case ERR:;
668         }
669
670         if (state->board.x <= 4) {
671                 update_timer(state, game_elapsed(&state->board));
672                 doupdate();
673         }
674 }
675
676 int main(int argc, char **argv)
677 {
678         setlocale(LC_ALL, "");
679         app_initialize(argc, argv);
680
681         curs_new_game(&state);
682         while (1)
683                 do_mainloop(&state);
684         abort();
685 }