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