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