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))
35 GAME_YPOS = 1 // top row of game and goal areas.
38 static const char *progname = "rrace";
39 static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
48 static struct app_state {
51 WINDOW *gamewin[WINDOW_MAX], *goalwin[WINDOW_MAX];
54 /* Most recently displayed timer value, for screen redraw. */
55 uint_least32_t timer_ms;
57 /* Location of the keyboard cursor */
60 /* If true, the goal will be displayed over the main play area. */
61 uint_least8_t view_goal_on_game;
63 /* Clicked toolbar item */
64 uint_least8_t toolbar_click;
67 static void print_version(void)
69 version_print_head("rrace-curses", stdout);
70 puts("License GPLv3+: GNU GPL version 3 or any later version");
71 puts("This is free software: you are free to change and redistribute it.");
72 puts("There is NO WARRANTY, to the extent permitted by law.");
75 static void print_usage(FILE *f)
77 fprintf(f, "Usage: %s [options]\n", progname);
79 fprintf(f, "Try %s --help for more information.\n", progname);
82 static void print_help(void)
84 struct lopt_help help = {0};
85 const struct option *opt;
91 for (opt = lopts; opt->name; opt++) {
92 if (!lopt_get_help(opt, &help))
94 help_print_option(opt, help.arg, help.desc, 20);
98 printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
101 static void draw_tile(WINDOW **win, unsigned colour, unsigned selected,
102 unsigned x, unsigned y, unsigned start_column)
104 WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
105 int w, h, attr, ch, bc = selected ? '#' : 0;
107 assert(colour < TILE_MAX);
108 attr = COLOR_PAIR(colour);
110 case TILE_RED: ch = 'X'; break;
111 case TILE_ORANGE: ch = '|'; break;
112 case TILE_GREEN: ch = '+'; break;
113 case TILE_YELLOW: ch = '~'; attr |= A_BOLD; break;
114 case TILE_BLUE: ch = 'o'; attr |= A_BOLD; break;
115 case TILE_WHITE: ch = '.'; attr |= A_BOLD; break;
117 case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(TILE_MAX);
120 getmaxyx(border, h, w);
123 if (mvwin(border, 1+GAME_YPOS+h*y, start_column+w*x) == ERR)
126 if (colour != TILE_EMPTY) {
127 mvderwin(fill, 1, 1);
128 wbkgdset(fill, A_REVERSE|attr|ch);
135 wattrset(border, attr);
136 wborder(border, bc, bc, bc, bc, bc, bc, bc, bc);
139 wnoutrefresh(border);
143 redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column,
144 uint_fast32_t bit0, uint_fast32_t bit1, uint_fast32_t bit2,
147 uint_fast32_t pos = board_position(x, y);
148 unsigned char tile = 0;
150 if (bit0 & pos) tile |= 1;
151 if (bit1 & pos) tile |= 2;
152 if (bit2 & pos) tile |= 4;
153 assert(tile < TILE_MAX);
155 draw_tile(win, tile, selected, x, y, start_column);
159 static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
161 int w, h, tr = 0, rs = 0, br = 0, bs = 0, bl = 0;
162 WINDOW *area = win[WINDOW_AREA];
164 getmaxyx(stdscr, h, w);
166 if (h <= GAME_YPOS+1) {
167 bl = ACS_ULCORNER, br = ACS_URCORNER;
168 } else if (h <= 3*sz+GAME_YPOS+1) {
169 bl = br = ACS_VLINE, bs = ' ';
173 tr = ACS_ULCORNER, br = ACS_LLCORNER;
174 } else if (w <= 6*sz+x+2) {
175 tr = ACS_HLINE, rs = ' ';
176 br = br ? ' ' : ACS_HLINE;
179 wborder(area, 0, rs, 0, bs, 0, tr, bl, br);
183 static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
185 uint_least32_t buf[3], *gp = state->board.game;
189 redraw_area_border(state->gamewin, 2, 5);
191 if (state->view_goal_on_game) {
192 for (i = 0; i < 3; i++) {
193 buf[i] = state->board.goal[i];
194 buf[i] = (gp[i] & ~GOAL_MASK) | (buf[i] << GOAL_SHIFT);
199 for (i = 0; i < 25; i++) {
201 redraw_tile(state->gamewin, i%5, i/5,
202 4, gp[0], gp[1], gp[2],
209 static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask)
211 uint_least16_t *gp = state->board.goal;
214 if (!state->goalwin[WINDOW_AREA])
217 getbegyx(state->goalwin[WINDOW_AREA], y, x);
219 redraw_area_border(state->goalwin, x, 3);
221 for (i = 0; i < 9; i++) {
223 redraw_tile(state->goalwin, i%3, i/3,
224 x+2, gp[0], gp[1], gp[2], 0);
230 static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x)
235 #if HAVE_CURSES_WRESIZE
236 if (wresize(win, h, w) != ERR) {
245 return *orig = subwin(stdscr, h, w, y, x);
249 static void realloc_tiles(WINDOW **win, int h)
251 WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
254 if (fill && border) {
257 #if HAVE_CURSES_WRESIZE
258 if (wresize(fill, h-2, w-2) != ERR
259 && wresize(border, h, w) != ERR)
265 getmaxyx(border, old_h, old_w);
275 win[WINDOW_TILEBORDER] = border = newwin(h, w, 0, 0);
276 win[WINDOW_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
279 static void setup_mainwin(struct app_state *state)
281 int w, h, gamesz, goalsz, scr_w, scr_h, split;
283 getmaxyx(stdscr, scr_h, scr_w);
285 /* First try to fit the game tiles based on window height. */
286 gamesz = MAX(3, (scr_h - 4) / 5);
288 /* Adjust downward until we can fit smallest possible goal area. */
289 for (; split = 5+10*gamesz, gamesz > 3; gamesz--) {
290 if (split + 20 < scr_w)
294 /* Pick a goal size that will fit in the remaining area */
295 goalsz = MAX(3, (scr_w - split - 4) / 6);
296 if (goalsz >= gamesz)
297 goalsz = MAX(3, gamesz - 1);
299 realloc_tiles(state->gamewin, gamesz);
300 realloc_tiles(state->goalwin, goalsz);
302 /* Frame for game area */
303 w = MIN(scr_w-2, 3+10*gamesz);
304 h = MIN(scr_h-GAME_YPOS, 2+5*gamesz);
305 realloc_area(&state->gamewin[WINDOW_AREA], h, w, GAME_YPOS, 2);
307 /* Frame for goal area */
308 w = MIN(scr_w-split, 3+6*goalsz);
309 h = MIN(scr_h-GAME_YPOS, 2+3*goalsz);
310 realloc_area(&state->goalwin[WINDOW_AREA], h, w, GAME_YPOS, split);
313 w = MAX(0, scr_w-split-1);
314 realloc_area(&state->timer, 1, w, GAME_YPOS+h, split+1);
317 static void app_initialize(int argc, char **argv)
319 int enable_mouse = 1;
325 while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) {
345 game_reset(&state.board);
353 keypad(stdscr, TRUE);
356 * While we only care about a subset of these events, for
357 * some reason with ncurses failing to enable all of them
358 * causes timeout to stop working when disabled buttons are
359 * pressed (or released).
361 * It is not known if other curses have this problem; at least
362 * pdcurses does not, but the extra events should be harmless
365 #if HAVE_CURSES_MOUSE_SUPPORT
366 unsigned long mask = BUTTON1_PRESSED | BUTTON1_RELEASED
367 | BUTTON2_PRESSED | BUTTON2_RELEASED
368 | BUTTON3_PRESSED | BUTTON3_RELEASED
369 #ifdef BUTTON4_PRESSED
370 | BUTTON4_PRESSED | BUTTON4_RELEASED
372 #ifdef BUTTON5_PRESSED
373 | BUTTON5_PRESSED | BUTTON5_RELEASED
376 #if HAVE_CURSES_MOUSE_SET
378 #elif HAVE_CURSES_MOUSEMASK
379 mousemask(mask, NULL);
381 #if HAVE_CURSES_MOUSEINTERVAL
384 #endif /* HAVE_CURSES_MOUSE_SUPPORT */
389 init_pair(TILE_RED, COLOR_RED, COLOR_BLACK);
390 init_pair(TILE_ORANGE, COLOR_YELLOW, COLOR_BLACK);
391 init_pair(TILE_YELLOW, COLOR_YELLOW, COLOR_BLACK);
392 init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
393 init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
394 init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
395 init_pair(TILE_MAX, COLOR_BLACK, COLOR_BLACK);
397 setup_mainwin(&state);
401 static void update_timer(struct app_state *state, uint_fast32_t ms)
405 state->timer_ms = ms;
406 sec = ms / 1000; ms %= 1000;
407 min = sec / 60; sec %= 60;
408 mvwprintw(state->timer, 0, 0, "Time: %u:%.2u.%.3u",
409 min, sec, (unsigned)ms);
410 wclrtoeol(state->timer);
411 wrefresh(state->timer);
414 static uint_fast32_t do_move(struct app_state *state, int x, int y)
418 if ((mask = game_do_move(&state->board, x, y)) != 0) {
419 uint_fast32_t goal = game_check_goal(&state->board);
421 if (state->view_goal_on_game) {
422 state->view_goal_on_game = 0;
428 update_timer(state, game_finish(&state->board));
434 curs_redraw_game(state, mask);
441 static void do_reset_cursor(struct app_state *state)
443 state->cursor = 5*state->board.y + state->board.x;
446 static void do_new_game(struct app_state *state)
448 game_reset(&state->board);
450 do_reset_cursor(state);
451 curs_redraw_game(state, -1);
452 curs_redraw_goal(state, -1);
453 update_timer(state, 0);
457 game_begin(&state->board);
460 #if HAVE_CURSES_MOUSE_SUPPORT
461 static void do_mouse(struct app_state *state)
463 unsigned long bstate;
466 #if HAVE_CURSES_GETMOUSE_NCURSES
469 if (getmouse(&mev) == ERR)
472 x = mev.x, y = mev.y;
474 #elif HAVE_CURSES_REQUEST_MOUSE_POS
481 #define set_bstate_helper(button) do { if (BUTTON_CHANGED(button)) \
482 switch (BUTTON_STATUS(button)) { \
483 case BUTTON_RELEASED: bstate |= BUTTON ## button ## _RELEASED; break; \
484 case BUTTON_PRESSED: bstate |= BUTTON ## button ## _PRESSED; break; \
488 set_bstate_helper(1);
489 set_bstate_helper(3);
491 if (bstate & BUTTON3_PRESSED) {
492 state->view_goal_on_game = 1;
493 curs_redraw_game(state, game_check_goal(&state->board));
497 if (bstate & BUTTON3_RELEASED) {
498 state->view_goal_on_game = 0;
499 curs_redraw_game(state, game_check_goal(&state->board));
503 if (!state->view_goal_on_game && bstate & BUTTON1_PRESSED) {
504 uint_fast32_t cursor_mask, move_mask;
507 /* Determine size of the game area */
508 getmaxyx(state->gamewin[WINDOW_TILEBORDER], h, w);
511 if (x < 4 || (x -= 4)/5 >= w) return;
512 if (y <= GAME_YPOS || (y -= GAME_YPOS+1)/5 >= h) return;
514 /* Turn off the keyboard cursor when using the mouse */
515 cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor;
518 move_mask = do_move(state, x/w, y/h);
519 if ((cursor_mask & move_mask) == 0) {
520 curs_redraw_game(state, cursor_mask);
527 static void do_move_cursor(struct app_state *state, int c)
529 uint_fast32_t mask = 0;
531 if (state->cursor < 0) {
532 /* Cursor was hidden; reset it */
533 do_reset_cursor(state);
535 mask = 1ul << state->cursor;
538 if (state->view_goal_on_game) {
539 state->view_goal_on_game = 0;
540 mask |= game_check_goal(&state->board);
545 if ((state->cursor -= 5) < 0)
549 if ((state->cursor += 5) >= 25)
553 if ((state->cursor -= 1) % 5 == 4 || state->cursor < 0)
557 if ((state->cursor += 1) % 5 == 0)
562 curs_redraw_game(state, mask | 1ul << state->cursor);
566 static void do_keystroke(struct app_state *state, int c)
569 case KEY_DOWN: case KEY_UP: case KEY_LEFT: case KEY_RIGHT:
570 do_move_cursor(state, c);
573 state->view_goal_on_game ^= 2u;
574 curs_redraw_game(state, game_check_goal(&state->board));
578 if (!(state->view_goal_on_game & 1) && state->cursor >= 0)
579 do_move(state, state->cursor%5, state->cursor/5);
584 int main(int argc, char **argv)
586 setlocale(LC_ALL, "");
587 app_initialize(argc, argv);
597 setup_mainwin(&state);
600 curs_redraw_game(&state, -1);
601 curs_redraw_goal(&state, -1);
602 update_timer(&state, state.timer_ms);
605 #if HAVE_CURSES_MOUSE_SUPPORT
611 do_keystroke(&state, c);
615 if (state.board.x <= 4)
616 update_timer(&state, game_elapsed(&state.board));