]> git.draconx.ca Git - rrace.git/blob - src/curses.c
Curses UI improvements.
[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 } state;
49
50 static void print_version(void)
51 {
52         version_print_head("rrace-curses", stdout);
53         puts("License GPLv3+: GNU GPL version 3 or any later version");
54         puts("This is free software: you are free to change and redistribute it.");
55         puts("There is NO WARRANTY, to the extent permitted by law.");
56 }
57
58 static void print_usage(FILE *f)
59 {
60         fprintf(f, "Usage: %s [options]\n", progname);
61         if (f != stdout)
62                 fprintf(f, "Try %s --help for more information.\n", progname);
63 }
64
65 static void print_help(void)
66 {
67         struct lopt_help help = {0};
68         const struct option *opt;
69
70         print_usage(stdout);
71
72         putchar('\n');
73         puts("Options:");
74         for (opt = lopts; opt->name; opt++) {
75                 if (!lopt_get_help(opt, &help))
76                         continue;
77                 help_print_option(opt, help.arg, help.desc, 20);
78         }
79         putchar('\n');
80
81         printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
82 }
83
84 static void draw_tile(WINDOW **win, unsigned colour,
85                       unsigned x, unsigned y, unsigned start_column)
86 {
87         WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
88         int attr, ch;
89         int w, h;
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
102         getmaxyx(border, h, w);
103         w = 2*(w+1)/2;
104
105         if (mvwin(border, 2+h*y, start_column+w*x) == ERR)
106                 return;
107
108         if (colour != TILE_EMPTY) {
109                 wattrset(border, attr);
110                 box(border, 0, 0);
111
112                 mvderwin(fill, 1, 1);
113                 wbkgdset(fill, A_REVERSE|attr|ch);
114                 werase(fill);
115         } else {
116                 werase(border);
117         }
118
119         wnoutrefresh(border);
120 }
121
122 static int
123 redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column,
124             uint_fast32_t bit0, uint_fast32_t bit1, uint_fast32_t bit2)
125 {
126         uint_fast32_t pos = board_position(x, y);
127         unsigned char tile = 0;
128
129         if (bit0 & pos) tile |= 1;
130         if (bit1 & pos) tile |= 2;
131         if (bit2 & pos) tile |= 4;
132         assert(tile < TILE_MAX);
133
134         draw_tile(win, tile, x, y, start_column);
135         return tile;
136 }
137
138 static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
139 {
140         int w, h, tr = 0, rs = 0, br = 0, bs = 0, bl = 0;
141         WINDOW *area = win[WINDOW_AREA];
142
143         getmaxyx(stdscr, h, w);
144
145         if (h <= 2) {
146                 bl = ACS_ULCORNER, br = ACS_URCORNER;
147         } else if (h <= 3*sz+2) {
148                 bl = br = ACS_VLINE, bs = ' ';
149         }
150
151         if (w <= x+1) {
152                 tr = ACS_ULCORNER, br = ACS_LLCORNER;
153         } else if (w <= 6*sz+x+2) {
154                 tr = ACS_HLINE, rs = ' ';
155                 br = br ? ' ' : ACS_HLINE;
156         }
157
158         wborder(area, 0, rs, 0, bs, 0, tr, bl, br);
159         wnoutrefresh(area);
160 }
161
162 static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
163 {
164         uint_least32_t *gp = state->board.game;
165         int i;
166
167         if (mask == -1)
168                 redraw_area_border(state->gamewin, 2, 5);
169
170         for (i = 0; i < 25; i++) {
171                 if (mask & 1) {
172                         redraw_tile(state->gamewin, i%5, i/5,
173                                     4, gp[0], gp[1], gp[2]);
174                 }
175                 mask >>= 1;
176         }
177 }
178
179 static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask)
180 {
181         uint_least16_t *gp = state->board.goal;
182         int i, x, y;
183
184         if (!state->goalwin[WINDOW_AREA])
185                 return;
186
187         getbegyx(state->goalwin[WINDOW_AREA], y, x);
188         if (mask == -1)
189                 redraw_area_border(state->goalwin, x, 3);
190
191         for (i = 0; i < 9; i++) {
192                 if (mask & 1) {
193                         redraw_tile(state->goalwin, i%3, i/3,
194                                     x+2, gp[0], gp[1], gp[2]);
195                 }
196                 mask >>= 1;
197         }
198 }
199
200 static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x)
201 {
202         WINDOW *win = *orig;
203
204         if (win) {
205 #if HAVE_CURSES_WRESIZE
206                 if (wresize(win, h, w) != ERR) {
207                         mvwin(win, y, x);
208                         return win;
209                 }
210 #endif
211                 delwin(win);
212         }
213
214         if (w > 0 && h > 0)
215                 return *orig = subwin(stdscr, h, w, y, x);
216         return *orig = NULL;
217 }
218
219 static void realloc_tiles(WINDOW **win, int h)
220 {
221         WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
222         int w = 2*h - 1;
223
224         if (fill && border) {
225                 int old_w, old_h;
226
227 #if HAVE_CURSES_WRESIZE
228                 if (wresize(fill, h-2, w-2) != ERR
229                     && wresize(border, h, w) != ERR)
230                 {
231                         return;
232                 }
233 #endif
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[WINDOW_TILEBORDER] = border = newwin(h, w, 0, 0);
246         win[WINDOW_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-1, 2+5*gamesz);
275         realloc_area(&state->gamewin[WINDOW_AREA], h, w, 1, 2);
276
277         /* Frame for goal area */
278         w = MIN(scr_w-split, 3+6*goalsz);
279         h = MIN(scr_h-1, 2+3*goalsz);
280         realloc_area(&state->goalwin[WINDOW_AREA], h, w, 1, split);
281 }
282
283 static void app_initialize(int argc, char **argv)
284 {
285         int enable_mouse = 1;
286         int opt;
287
288         if (argc > 0)
289                 progname = argv[0];
290
291         while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) {
292                 switch (opt) {
293                 case LOPT_MOUSE:
294                         enable_mouse = 2;
295                         break;
296                 case LOPT_NO_MOUSE:
297                         enable_mouse = 0;
298                         break;
299                 case LOPT_VERSION:
300                         print_version();
301                         exit(EXIT_SUCCESS);
302                 case LOPT_HELP:
303                         print_help();
304                         exit(EXIT_SUCCESS);
305                 default:
306                         print_usage(stderr);
307                         exit(EXIT_FAILURE);
308                 }
309         }
310
311         game_reset(&state.board);
312
313         initscr();
314         start_color();
315         if (curs_set(0) != ERR)
316                 leaveok(stdscr, TRUE);
317
318         cbreak();
319         keypad(stdscr, TRUE);
320         if (enable_mouse) {
321 #if HAVE_CURSES_MOUSE_SET
322                 mouse_set(BUTTON1_PRESSED);
323 #elif HAVE_CURSES_MOUSEMASK
324                 mousemask(BUTTON1_PRESSED, NULL);
325 #endif
326 #if HAVE_CURSES_MOUSEINTERVAL
327                 mouseinterval(0);
328 #endif
329         }
330
331         noecho();
332
333         init_pair(TILE_RED, COLOR_RED, COLOR_BLACK);
334         init_pair(TILE_ORANGE, COLOR_YELLOW, COLOR_BLACK);
335         init_pair(TILE_YELLOW, COLOR_YELLOW, COLOR_BLACK);
336         init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
337         init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
338         init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
339
340         setup_mainwin(&state);
341         refresh();
342 }
343
344 static void do_move(struct app_state *state, int x, int y)
345 {
346         uint_fast32_t mask;
347
348         if ((mask = game_do_move(&state->board, x, y)) != 0) {
349                 curs_redraw_game(state, mask);
350                 refresh();
351         }
352 }
353
354 #if HAVE_CURSES_MOUSE_SUPPORT
355 static void do_mouse(struct app_state *state)
356 {
357         unsigned long bstate;
358         int x, y;
359
360 #if HAVE_CURSES_GETMOUSE_NCURSES
361         MEVENT mev;
362
363         if (getmouse(&mev) == ERR)
364                 return;
365
366         x = mev.x, y = mev.y;
367         bstate = mev.bstate;
368 #elif HAVE_CURSES_REQUEST_MOUSE_POS
369         request_mouse_pos();
370         x = MOUSE_X_POS;
371         y = MOUSE_Y_POS;
372
373         bstate = 0;
374         if (BUTTON_CHANGED(1)) {
375                 switch (BUTTON_STATUS(1)) {
376                 case BUTTON_RELEASED: bstate |= BUTTON1_RELEASED;
377                 case BUTTON_PRESSED:  bstate |= BUTTON1_PRESSED;
378                 }
379         }
380 #endif
381         if (bstate == BUTTON1_PRESSED) {
382                 int w, h;
383
384                 /* Determine size of the game area */
385                 getmaxyx(state->gamewin[WINDOW_TILEBORDER], h, w);
386                 w = 2*(w+1)/2;
387
388                 if (x < 4 || (x -= 4)/5 >= w) return;
389                 if (y < 2 || (y -= 2)/5 >= h) return;
390
391                 do_move(state, x/w, y/h);
392         }
393 }
394 #endif
395
396 int main(int argc, char **argv)
397 {
398         setlocale(LC_ALL, "");
399         app_initialize(argc, argv);
400
401         curs_redraw_game(&state, -1);
402         curs_redraw_goal(&state, -1);
403         refresh();
404
405         while (1) {
406                 int c = getch();
407
408                 switch (c) {
409 #ifdef KEY_RESIZE
410                 case KEY_RESIZE:
411                         setup_mainwin(&state);
412                         clear();
413                         refresh();
414                         curs_redraw_game(&state, -1);
415                         curs_redraw_goal(&state, -1);
416                         refresh();
417                         break;
418 #endif
419 #if HAVE_CURSES_MOUSE_SUPPORT
420                 case KEY_MOUSE:
421                         do_mouse(&state);
422                         break;
423 #endif
424                 }
425         }
426 }