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