]> git.draconx.ca Git - rrace.git/blob - src/curses.c
curses: Add timer display.
[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 #include "cursesopt.h"
29 #include "game.h"
30
31 #define MIN(a, b) ((a) < (b) ? (a) : (b))
32 #define MAX(a, b) ((a) > (b) ? (a) : (b))
33
34 enum {
35         GAME_YPOS = 1 // top row of game and goal areas.
36 };
37
38 static const char *progname = "rrace";
39 static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
40
41 enum {
42         WINDOW_TILEBORDER,
43         WINDOW_TILEFILL,
44         WINDOW_AREA,
45         WINDOW_MAX,
46 };
47
48 static struct app_state {
49         struct board board;
50
51         WINDOW *gamewin[WINDOW_MAX], *goalwin[WINDOW_MAX];
52         WINDOW *timer;
53         int cursor;
54 } state;
55
56 static void print_version(void)
57 {
58         version_print_head("rrace-curses", stdout);
59         puts("License GPLv3+: GNU GPL version 3 or any later version");
60         puts("This is free software: you are free to change and redistribute it.");
61         puts("There is NO WARRANTY, to the extent permitted by law.");
62 }
63
64 static void print_usage(FILE *f)
65 {
66         fprintf(f, "Usage: %s [options]\n", progname);
67         if (f != stdout)
68                 fprintf(f, "Try %s --help for more information.\n", progname);
69 }
70
71 static void print_help(void)
72 {
73         struct lopt_help help = {0};
74         const struct option *opt;
75
76         print_usage(stdout);
77
78         putchar('\n');
79         puts("Options:");
80         for (opt = lopts; opt->name; opt++) {
81                 if (!lopt_get_help(opt, &help))
82                         continue;
83                 help_print_option(opt, help.arg, help.desc, 20);
84         }
85         putchar('\n');
86
87         printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
88 }
89
90 static void draw_tile(WINDOW **win, unsigned colour, unsigned selected,
91                       unsigned x, unsigned y, unsigned start_column)
92 {
93         WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
94         int w, h, attr, ch, bc = selected ? '#' : 0;
95
96         assert(colour < TILE_MAX);
97         attr = COLOR_PAIR(colour);
98         switch (colour) {
99         case TILE_RED:    ch = 'X'; break;
100         case TILE_ORANGE: ch = '|'; break;
101         case TILE_GREEN:  ch = '+'; break;
102         case TILE_YELLOW: ch = '~'; attr |= A_BOLD; break;
103         case TILE_BLUE:   ch = 'o'; attr |= A_BOLD; break;
104         case TILE_WHITE:  ch = '.'; attr |= A_BOLD; break;
105
106         case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(TILE_MAX);
107         }
108
109         getmaxyx(border, h, w);
110         w = 2*(w+1)/2;
111
112         if (mvwin(border, 1+GAME_YPOS+h*y, start_column+w*x) == ERR)
113                 return;
114
115         if (colour != TILE_EMPTY) {
116                 mvderwin(fill, 1, 1);
117                 wbkgdset(fill, A_REVERSE|attr|ch);
118                 werase(fill);
119         } else {
120                 werase(border);
121         }
122
123         if (bc || colour) {
124                 wattrset(border, attr);
125                 wborder(border, bc, bc, bc, bc, bc, bc, bc, bc);
126         }
127
128         wnoutrefresh(border);
129 }
130
131 static int
132 redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column,
133             uint_fast32_t bit0, uint_fast32_t bit1, uint_fast32_t bit2,
134             unsigned selected)
135 {
136         uint_fast32_t pos = board_position(x, y);
137         unsigned char tile = 0;
138
139         if (bit0 & pos) tile |= 1;
140         if (bit1 & pos) tile |= 2;
141         if (bit2 & pos) tile |= 4;
142         assert(tile < TILE_MAX);
143
144         draw_tile(win, tile, selected, x, y, start_column);
145         return tile;
146 }
147
148 static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
149 {
150         int w, h, tr = 0, rs = 0, br = 0, bs = 0, bl = 0;
151         WINDOW *area = win[WINDOW_AREA];
152
153         getmaxyx(stdscr, h, w);
154
155         if (h <= GAME_YPOS+1) {
156                 bl = ACS_ULCORNER, br = ACS_URCORNER;
157         } else if (h <= 3*sz+GAME_YPOS+1) {
158                 bl = br = ACS_VLINE, bs = ' ';
159         }
160
161         if (w <= x+1) {
162                 tr = ACS_ULCORNER, br = ACS_LLCORNER;
163         } else if (w <= 6*sz+x+2) {
164                 tr = ACS_HLINE, rs = ' ';
165                 br = br ? ' ' : ACS_HLINE;
166         }
167
168         wborder(area, 0, rs, 0, bs, 0, tr, bl, br);
169         wnoutrefresh(area);
170 }
171
172 static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
173 {
174         uint_least32_t *gp = state->board.game;
175         int i;
176
177         if (mask == -1)
178                 redraw_area_border(state->gamewin, 2, 5);
179
180         for (i = 0; i < 25; i++) {
181                 if (mask & 1) {
182                         redraw_tile(state->gamewin, i%5, i/5,
183                                     4, gp[0], gp[1], gp[2],
184                                     i == state->cursor);
185                 }
186                 mask >>= 1;
187         }
188 }
189
190 static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask)
191 {
192         uint_least16_t *gp = state->board.goal;
193         int i, x, y;
194
195         if (!state->goalwin[WINDOW_AREA])
196                 return;
197
198         getbegyx(state->goalwin[WINDOW_AREA], y, x);
199         if (mask == -1)
200                 redraw_area_border(state->goalwin, x, 3);
201
202         for (i = 0; i < 9; i++) {
203                 if (mask & 1) {
204                         redraw_tile(state->goalwin, i%3, i/3,
205                                     x+2, gp[0], gp[1], gp[2], 0);
206                 }
207                 mask >>= 1;
208         }
209 }
210
211 static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x)
212 {
213         WINDOW *win = *orig;
214
215         if (win) {
216 #if HAVE_CURSES_WRESIZE
217                 if (wresize(win, h, w) != ERR) {
218                         mvwin(win, y, x);
219                         return win;
220                 }
221 #endif
222                 delwin(win);
223         }
224
225         if (w > 0 && h > 0)
226                 return *orig = subwin(stdscr, h, w, y, x);
227         return *orig = NULL;
228 }
229
230 static void realloc_tiles(WINDOW **win, int h)
231 {
232         WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
233         int w = 2*h - 1;
234
235         if (fill && border) {
236                 int old_w, old_h;
237
238 #if HAVE_CURSES_WRESIZE
239                 if (wresize(fill, h-2, w-2) != ERR
240                     && wresize(border, h, w) != ERR)
241                 {
242                         return;
243                 }
244 #endif
245
246                 getmaxyx(border, old_h, old_w);
247                 if (old_h == h)
248                         return;
249         }
250
251         if (fill)
252                 delwin(fill);
253         if (border)
254                 delwin(border);
255
256         win[WINDOW_TILEBORDER] = border = newwin(h, w, 0, 0);
257         win[WINDOW_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
258 }
259
260 static void setup_mainwin(struct app_state *state)
261 {
262         int w, h, gamesz, goalsz, scr_w, scr_h, split;
263
264         getmaxyx(stdscr, scr_h, scr_w);
265
266         /* First try to fit the game tiles based on window height. */
267         gamesz = MAX(3, (scr_h - 4) / 5);
268
269         /* Adjust downward until we can fit smallest possible goal area. */
270         for (; split = 5+10*gamesz, gamesz > 3; gamesz--) {
271                 if (split + 20 < scr_w)
272                         break;
273         }
274
275         /* Pick a goal size that will fit in the remaining area */
276         goalsz = MAX(3, (scr_w - split - 4) / 6);
277         if (goalsz >= gamesz)
278                 goalsz = MAX(3, gamesz - 1);
279
280         realloc_tiles(state->gamewin, gamesz);
281         realloc_tiles(state->goalwin, goalsz);
282
283         /* Frame for game area */
284         w = MIN(scr_w-2, 3+10*gamesz);
285         h = MIN(scr_h-GAME_YPOS, 2+5*gamesz);
286         realloc_area(&state->gamewin[WINDOW_AREA], h, w, GAME_YPOS, 2);
287
288         /* Frame for goal area */
289         w = MIN(scr_w-split, 3+6*goalsz);
290         h = MIN(scr_h-GAME_YPOS, 2+3*goalsz);
291         realloc_area(&state->goalwin[WINDOW_AREA], h, w, GAME_YPOS, split);
292
293         /* Status area */
294         w = MAX(0, scr_w-split-1);
295         realloc_area(&state->timer, 1, w, GAME_YPOS+h, split+1);
296 }
297
298 static void app_initialize(int argc, char **argv)
299 {
300         int enable_mouse = 1;
301         int opt;
302
303         if (argc > 0)
304                 progname = argv[0];
305
306         while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) {
307                 switch (opt) {
308                 case LOPT_MOUSE:
309                         enable_mouse = 2;
310                         break;
311                 case LOPT_NO_MOUSE:
312                         enable_mouse = 0;
313                         break;
314                 case LOPT_VERSION:
315                         print_version();
316                         exit(EXIT_SUCCESS);
317                 case LOPT_HELP:
318                         print_help();
319                         exit(EXIT_SUCCESS);
320                 default:
321                         print_usage(stderr);
322                         exit(EXIT_FAILURE);
323                 }
324         }
325
326         game_reset(&state.board);
327         state.cursor = -1;
328
329         initscr();
330         start_color();
331         curs_set(0);
332
333         cbreak();
334         keypad(stdscr, TRUE);
335         if (enable_mouse) {
336                 /*
337                  * While we only care about a subset of these events, for
338                  * some reason with ncurses failing to enable all of them
339                  * causes timeout to stop working when disabled buttons are
340                  * pressed (or released).
341                  *
342                  * It is not known if other curses have this problem; at least
343                  * pdcurses does not, but the extra events should be harmless
344                  * in any case.
345                  */
346 #if HAVE_CURSES_MOUSE_SUPPORT
347                 unsigned long mask = BUTTON1_PRESSED | BUTTON1_RELEASED
348                                    | BUTTON2_PRESSED | BUTTON2_RELEASED
349                                    | BUTTON3_PRESSED | BUTTON3_RELEASED
350 #ifdef BUTTON4_PRESSED
351                                    | BUTTON4_PRESSED | BUTTON4_RELEASED
352 #endif
353 #ifdef BUTTON5_PRESSED
354                                    | BUTTON5_PRESSED | BUTTON5_RELEASED
355 #endif
356                                    ;
357 #if HAVE_CURSES_MOUSE_SET
358                 mouse_set(mask);
359 #elif HAVE_CURSES_MOUSEMASK
360                 mousemask(mask, NULL);
361 #endif
362 #if HAVE_CURSES_MOUSEINTERVAL
363                 mouseinterval(0);
364 #endif
365 #endif /* HAVE_CURSES_MOUSE_SUPPORT */
366         }
367
368         noecho();
369
370         init_pair(TILE_RED, COLOR_RED, COLOR_BLACK);
371         init_pair(TILE_ORANGE, COLOR_YELLOW, COLOR_BLACK);
372         init_pair(TILE_YELLOW, COLOR_YELLOW, COLOR_BLACK);
373         init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
374         init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
375         init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
376         init_pair(TILE_MAX, COLOR_BLACK, COLOR_BLACK);
377
378         setup_mainwin(&state);
379         refresh();
380 }
381
382 static uint_fast32_t do_move(struct app_state *state, int x, int y)
383 {
384         uint_fast32_t mask;
385
386         if ((mask = game_do_move(&state->board, x, y)) != 0) {
387                 curs_redraw_game(state, mask);
388                 doupdate();
389         }
390
391         return mask;
392 }
393
394 static void do_reset_cursor(struct app_state *state)
395 {
396         state->cursor = 5*state->board.y + state->board.x;
397 }
398
399 static void update_timer(struct app_state *state, uint_fast32_t ms)
400 {
401         unsigned sec, min;
402
403         sec = ms / 1000; ms %= 1000;
404         min = sec / 60; sec %= 60;
405         mvwprintw(state->timer, 0, 0, "Time: %u:%.2u.%.3u",
406                   min, sec, (unsigned)ms);
407         wclrtoeol(state->timer);
408         wrefresh(state->timer);
409 }
410
411 static void do_new_game(struct app_state *state)
412 {
413         game_reset(&state->board);
414
415         do_reset_cursor(state);
416         curs_redraw_game(state, -1);
417         curs_redraw_goal(state, -1);
418         update_timer(state, 0);
419         doupdate();
420
421         timeout(33);
422         game_begin(&state->board);
423 }
424
425 #if HAVE_CURSES_MOUSE_SUPPORT
426 static void do_mouse(struct app_state *state)
427 {
428         unsigned long bstate;
429         int x, y;
430
431 #if HAVE_CURSES_GETMOUSE_NCURSES
432         MEVENT mev;
433
434         if (getmouse(&mev) == ERR)
435                 return;
436
437         x = mev.x, y = mev.y;
438         bstate = mev.bstate;
439 #elif HAVE_CURSES_REQUEST_MOUSE_POS
440         request_mouse_pos();
441         x = MOUSE_X_POS;
442         y = MOUSE_Y_POS;
443
444         bstate = 0;
445
446 #define set_bstate_helper(button) do { if (BUTTON_CHANGED(button)) \
447         switch (BUTTON_STATUS(button)) { \
448         case BUTTON_RELEASED: bstate |= BUTTON ## button ##  _RELEASED; break; \
449         case BUTTON_PRESSED:  bstate |= BUTTON ## button ##  _PRESSED; break; \
450         } \
451 } while (0);
452
453         set_bstate_helper(1);
454         set_bstate_helper(3);
455 #endif
456         if (bstate == BUTTON1_PRESSED) {
457                 uint_fast32_t cursor_mask, move_mask;
458                 int w, h;
459
460                 /* Determine size of the game area */
461                 getmaxyx(state->gamewin[WINDOW_TILEBORDER], h, w);
462                 w = 2*(w+1)/2;
463
464                 if (x < 4 || (x -= 4)/5 >= w) return;
465                 if (y <= GAME_YPOS || (y -= GAME_YPOS+1)/5 >= h) return;
466
467                 /* Turn off the keyboard cursor when using the mouse */
468                 cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor;
469                 state->cursor = -1;
470
471                 move_mask = do_move(state, x/w, y/h);
472                 if ((cursor_mask & move_mask) == 0) {
473                         curs_redraw_game(state, cursor_mask);
474                         doupdate();
475                 }
476         }
477 }
478 #endif
479
480 static void do_move_cursor(struct app_state *state, int c)
481 {
482         uint_fast32_t mask = 1ul << state->cursor;
483
484         if (state->cursor < 0) {
485                 /* Cursor was hidden; reset it */
486                 do_reset_cursor(state);
487         }
488
489         switch (c) {
490         case KEY_UP:
491                 if ((state->cursor -= 5) < 0)
492                         state->cursor += 25;
493                 break;
494         case KEY_DOWN:
495                 if ((state->cursor += 5) >= 25)
496                         state->cursor -= 25;
497                 break;
498         case KEY_LEFT:
499                 if ((state->cursor -= 1) % 5 == 4 || state->cursor < 0)
500                         state->cursor += 5;
501                 break;
502         case KEY_RIGHT:
503                 if ((state->cursor += 1) % 5 == 0)
504                         state->cursor -= 5;
505                 break;
506         }
507
508         curs_redraw_game(state, mask | 1ul << state->cursor);
509         refresh();
510 }
511
512 static void do_keystroke(struct app_state *state, int c)
513 {
514         switch (c) {
515         case KEY_DOWN: case KEY_UP: case KEY_LEFT: case KEY_RIGHT:
516                 do_move_cursor(state, c);
517                 break;
518         case ' ':
519                 if (state->cursor >= 0)
520                         do_move(state, state->cursor%5, state->cursor/5);
521                 break;
522         }
523 }
524
525 int main(int argc, char **argv)
526 {
527         setlocale(LC_ALL, "");
528         app_initialize(argc, argv);
529
530         do_new_game(&state);
531
532         while (1) {
533                 int c = getch();
534
535                 switch (c) {
536 #ifdef KEY_RESIZE
537                 case KEY_RESIZE:
538                         setup_mainwin(&state);
539                         clear();
540                         refresh();
541                         curs_redraw_game(&state, -1);
542                         curs_redraw_goal(&state, -1);
543                         refresh();
544                         break;
545 #endif
546 #if HAVE_CURSES_MOUSE_SUPPORT
547                 case KEY_MOUSE:
548                         do_mouse(&state);
549                         break;
550 #endif
551                 default:
552                         do_keystroke(&state, c);
553                 case ERR:;
554                 }
555
556                 update_timer(&state, game_elapsed(&state.board));
557         }
558 }