2 * Curses UI for slide puzzle game
3 * Copyright © 2022 Nick Bowler
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.
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.
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/>.
28 #include "cursesopt.h"
31 #define MIN(a, b) ((a) < (b) ? (a) : (b))
32 #define MAX(a, b) ((a) > (b) ? (a) : (b))
34 static const char *progname = "rrace";
35 static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
44 static struct app_state {
47 WINDOW *gamewin[WINDOW_MAX], *goalwin[WINDOW_MAX];
50 static void print_version(void)
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.");
58 static void print_usage(FILE *f)
60 fprintf(f, "Usage: %s [options]\n", progname);
62 fprintf(f, "Try %s --help for more information.\n", progname);
65 static void print_help(void)
67 struct lopt_help help = {0};
68 const struct option *opt;
74 for (opt = lopts; opt->name; opt++) {
75 if (!lopt_get_help(opt, &help))
77 help_print_option(opt, help.arg, help.desc, 20);
81 printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
84 static void draw_tile(WINDOW **win, unsigned colour,
85 unsigned x, unsigned y, unsigned start_column)
87 WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
91 assert(colour < TILE_MAX);
92 attr = COLOR_PAIR(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;
102 getmaxyx(border, h, w);
105 if (mvwin(border, 2+h*y, start_column+w*x) == ERR)
108 if (colour != TILE_EMPTY) {
109 wattrset(border, attr);
112 mvderwin(fill, 1, 1);
113 wbkgdset(fill, A_REVERSE|attr|ch);
119 wnoutrefresh(border);
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)
126 uint_fast32_t pos = board_position(x, y);
127 unsigned char tile = 0;
129 if (bit0 & pos) tile |= 1;
130 if (bit1 & pos) tile |= 2;
131 if (bit2 & pos) tile |= 4;
132 assert(tile < TILE_MAX);
134 draw_tile(win, tile, x, y, start_column);
138 static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
140 int w, h, tr = 0, rs = 0, br = 0, bs = 0, bl = 0;
141 WINDOW *area = win[WINDOW_AREA];
143 getmaxyx(stdscr, h, w);
146 bl = ACS_ULCORNER, br = ACS_URCORNER;
147 } else if (h <= 3*sz+2) {
148 bl = br = ACS_VLINE, bs = ' ';
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;
158 wborder(area, 0, rs, 0, bs, 0, tr, bl, br);
162 static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
164 uint_least32_t *gp = state->board.game;
168 redraw_area_border(state->gamewin, 2, 5);
170 for (i = 0; i < 25; i++) {
172 redraw_tile(state->gamewin, i%5, i/5,
173 4, gp[0], gp[1], gp[2]);
179 static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask)
181 uint_least16_t *gp = state->board.goal;
184 if (!state->goalwin[WINDOW_AREA])
187 getbegyx(state->goalwin[WINDOW_AREA], y, x);
189 redraw_area_border(state->goalwin, x, 3);
191 for (i = 0; i < 9; i++) {
193 redraw_tile(state->goalwin, i%3, i/3,
194 x+2, gp[0], gp[1], gp[2]);
200 static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x)
205 #if HAVE_CURSES_WRESIZE
206 if (wresize(win, h, w) != ERR) {
215 return *orig = subwin(stdscr, h, w, y, x);
219 static void realloc_tiles(WINDOW **win, int h)
221 WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
224 if (fill && border) {
227 #if HAVE_CURSES_WRESIZE
228 if (wresize(fill, h-2, w-2) != ERR
229 && wresize(border, h, w) != ERR)
235 getmaxyx(border, old_h, old_w);
245 win[WINDOW_TILEBORDER] = border = newwin(h, w, 0, 0);
246 win[WINDOW_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
249 static void setup_mainwin(struct app_state *state)
251 int w, h, gamesz, goalsz, scr_w, scr_h, split;
253 getmaxyx(stdscr, scr_h, scr_w);
255 /* First try to fit the game tiles based on window height. */
256 gamesz = MAX(3, (scr_h - 4) / 5);
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)
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);
269 realloc_tiles(state->gamewin, gamesz);
270 realloc_tiles(state->goalwin, goalsz);
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);
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);
283 static void app_initialize(int argc, char **argv)
285 int enable_mouse = 1;
291 while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) {
311 game_reset(&state.board);
315 if (curs_set(0) != ERR)
316 leaveok(stdscr, TRUE);
319 keypad(stdscr, TRUE);
321 #if HAVE_CURSES_MOUSE_SET
322 mouse_set(BUTTON1_PRESSED);
323 #elif HAVE_CURSES_MOUSEMASK
324 mousemask(BUTTON1_PRESSED, NULL);
326 #if HAVE_CURSES_MOUSEINTERVAL
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);
340 setup_mainwin(&state);
344 static void do_move(struct app_state *state, int x, int y)
348 if ((mask = game_do_move(&state->board, x, y)) != 0) {
349 curs_redraw_game(state, mask);
354 #if HAVE_CURSES_MOUSE_SUPPORT
355 static void do_mouse(struct app_state *state)
357 unsigned long bstate;
360 #if HAVE_CURSES_GETMOUSE_NCURSES
363 if (getmouse(&mev) == ERR)
366 x = mev.x, y = mev.y;
368 #elif HAVE_CURSES_REQUEST_MOUSE_POS
374 if (BUTTON_CHANGED(1)) {
375 switch (BUTTON_STATUS(1)) {
376 case BUTTON_RELEASED: bstate |= BUTTON1_RELEASED;
377 case BUTTON_PRESSED: bstate |= BUTTON1_PRESSED;
381 if (bstate == BUTTON1_PRESSED) {
384 /* Determine size of the game area */
385 getmaxyx(state->gamewin[WINDOW_TILEBORDER], h, w);
388 if (x < 4 || (x -= 4)/5 >= w) return;
389 if (y < 2 || (y -= 2)/5 >= h) return;
391 do_move(state, x/w, y/h);
396 int main(int argc, char **argv)
398 setlocale(LC_ALL, "");
399 app_initialize(argc, argv);
401 curs_redraw_game(&state, -1);
402 curs_redraw_goal(&state, -1);
411 setup_mainwin(&state);
414 curs_redraw_game(&state, -1);
415 curs_redraw_goal(&state, -1);
419 #if HAVE_CURSES_MOUSE_SUPPORT