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