]> git.draconx.ca Git - rrace.git/blob - src/curses.c
Add initial curses-based 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 static const char *progname = "rrace";
32 static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
33
34 static struct app_state {
35         struct board board;
36
37         WINDOW *tile_border, *tile_fill;
38 } state;
39
40 static void print_version(void)
41 {
42         version_print_head("rrace-curses", stdout);
43         puts("License GPLv3+: GNU GPL version 3 or any later version");
44         puts("This is free software: you are free to change and redistribute it.");
45         puts("There is NO WARRANTY, to the extent permitted by law.");
46 }
47
48 static void print_usage(FILE *f)
49 {
50         fprintf(f, "Usage: %s [options]\n", progname);
51         if (f != stdout)
52                 fprintf(f, "Try %s --help for more information.\n", progname);
53 }
54
55 static void print_help(void)
56 {
57         struct lopt_help help = {0};
58         const struct option *opt;
59
60         print_usage(stdout);
61
62         putchar('\n');
63         puts("Options:");
64         for (opt = lopts; opt->name; opt++) {
65                 if (!lopt_get_help(opt, &help))
66                         continue;
67                 help_print_option(opt, help.arg, help.desc, 20);
68         }
69         putchar('\n');
70
71         printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
72 }
73
74 static void
75 draw_tile(struct app_state *state, unsigned colour, unsigned x, unsigned y)
76 {
77         int attr, ch;
78         int w, h;
79
80         assert(colour < TILE_MAX);
81         attr = COLOR_PAIR(colour);
82         switch (colour) {
83         case TILE_RED:    ch = 'X'; break;
84         case TILE_ORANGE: ch = '|'; break;
85         case TILE_GREEN:  ch = '+'; break;
86         case TILE_YELLOW: ch = '~'; attr |= A_BOLD; break;
87         case TILE_BLUE:   ch = 'o'; attr |= A_BOLD; break;
88         case TILE_WHITE:  ch = '.'; attr |= A_BOLD; break;
89         }
90
91         getmaxyx(state->tile_border, h, w);
92         w = 2*(w+1)/2;
93
94         if (mvwin(state->tile_border, 2+h*y, 4+w*x) == ERR)
95                 return;
96
97         if (colour != TILE_EMPTY) {
98                 wattrset(state->tile_border, attr);
99                 box(state->tile_border, 0, 0);
100
101                 mvderwin(state->tile_fill, 1, 1);
102                 wbkgdset(state->tile_fill, A_REVERSE|attr|ch);
103                 werase(state->tile_fill);
104         } else {
105                 werase(state->tile_border);
106         }
107
108         wnoutrefresh(state->tile_border);
109 }
110
111 static int curs_redraw_tile(struct app_state *state, unsigned x, unsigned y)
112 {
113         uint_fast32_t pos = board_position(x, y);
114         unsigned char tile = 0;
115
116         if (state->board.game[0] & pos) tile |= 1;
117         if (state->board.game[1] & pos) tile |= 2;
118         if (state->board.game[2] & pos) tile |= 4;
119         assert(tile < TILE_MAX);
120
121         draw_tile(state, tile, x, y);
122         return tile;
123 }
124
125 static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
126 {
127         int i;
128
129         for (i = 0; i < 25; i++) {
130                 if (mask & 1) {
131                         curs_redraw_tile(state, i%5, i/5);
132                 }
133                 mask >>= 1;
134         }
135 }
136
137 static void curs_alloc_tiles(struct app_state *state)
138 {
139         int w, h, tilesz;
140         WINDOW *tile;
141
142         getmaxyx(stdscr, h, w);
143         tilesz = (h - 4) / 5;
144         if (tilesz < 3)
145                 tilesz = 3;
146
147         h = -1;
148         if (state->tile_border) {
149                 getmaxyx(state->tile_border, h, w);
150         }
151
152         if (h == tilesz) {
153                 /* Nothing to do. */
154                 return;
155         }
156
157         if (state->tile_border) {
158                 delwin(state->tile_fill);
159                 delwin(state->tile_border);
160         }
161
162         state->tile_border = tile = newwin(tilesz, 2*tilesz-1, 0, 0);
163         state->tile_fill = derwin(tile, tilesz-2, 2*tilesz-3, 1, 1);
164 }
165
166 static void app_initialize(int argc, char **argv)
167 {
168         int opt;
169
170         if (argc > 0)
171                 progname = argv[0];
172
173         while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) {
174                 switch (opt) {
175                 case LOPT_VERSION:
176                         print_version();
177                         exit(EXIT_SUCCESS);
178                 case LOPT_HELP:
179                         print_help();
180                         exit(EXIT_SUCCESS);
181                 default:
182                         print_usage(stderr);
183                         exit(EXIT_FAILURE);
184                 }
185         }
186
187         game_reset(&state.board);
188
189         initscr();
190         start_color();
191         if (curs_set(0) != ERR)
192                 leaveok(stdscr, TRUE);
193
194         cbreak();
195         keypad(stdscr, TRUE);
196         mousemask(BUTTON1_PRESSED, NULL);
197         mouseinterval(0);
198         noecho();
199
200         init_pair(TILE_RED, COLOR_RED, COLOR_BLACK);
201         init_pair(TILE_ORANGE, COLOR_YELLOW, COLOR_BLACK);
202         init_pair(TILE_YELLOW, COLOR_YELLOW, COLOR_BLACK);
203         init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
204         init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
205         init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
206
207         curs_alloc_tiles(&state);
208         refresh();
209 }
210
211 static void do_move(struct app_state *state, int x, int y)
212 {
213         uint_fast32_t mask;
214
215         if ((mask = game_do_move(&state->board, x, y)) != 0) {
216                 curs_redraw_game(state, mask);
217                 refresh();
218         }
219 }
220
221 static void do_mouse(struct app_state *state, MEVENT *mev)
222 {
223         if (mev->bstate == BUTTON1_PRESSED) {
224                 int w, h, x, y;
225
226                 /* Determine size of the game area */
227                 getmaxyx(state->tile_border, h, w);
228                 w = 2*(w+1)/2;
229
230                 if (mev->x < 4 || (x = mev->x - 4)/5 >= w) return;
231                 if (mev->y < 2 || (y = mev->y - 2)/5 >= h) return;
232
233                 do_move(state, x/w, y/h);
234         }
235 }
236
237 int main(int argc, char **argv)
238 {
239         setlocale(LC_ALL, "");
240         app_initialize(argc, argv);
241
242         curs_redraw_game(&state, -1);
243         refresh();
244
245         while (1) {
246                 int c = getch();
247                 MEVENT mev;
248
249                 switch (c) {
250                 case KEY_RESIZE:
251                         curs_alloc_tiles(&state);
252                         clear();
253                         refresh();
254                         curs_redraw_game(&state, -1);
255                         refresh();
256                         break;
257                 case KEY_MOUSE:
258                         if (getmouse(&mev) != ERR)
259                                 do_mouse(&state, &mev);
260                         break;
261                 }
262         }
263 }