]> git.draconx.ca Git - rrace.git/blob - src/curses.c
curses: Begin to implement a game menu system.
[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 /* Colour pair enumeration */
49 enum {
50         /* Pairs 1-6 correspond to tile colours */
51         RR_COLOUR_CURSOR = TILE_MAX, // black on black, for the cursor
52         RR_COLOUR_TOOLBAR,           // cyan on black (use reverse video)
53         RR_COLOUR_MAX
54 };
55
56 static struct app_state {
57         struct board board;
58
59         WINDOW *gamewin[WINDOW_MAX], *goalwin[WINDOW_MAX];
60         WINDOW *toolbar, *timer;
61         int last_input;
62
63         /* Most recently displayed timer value, for screen redraw. */
64         uint_least32_t timer_ms;
65
66         /* Location of the keyboard cursor */
67         int_least8_t cursor;
68
69         /* If true, the goal will be displayed over the main play area. */
70         uint_least8_t view_goal_on_game;
71
72         /* Clicked toolbar item */
73         uint_least8_t toolbar_click;
74 } state;
75
76 static void print_version(void)
77 {
78         version_print_head("rrace-curses", stdout);
79         puts("License GPLv3+: GNU GPL version 3 or any later version");
80         puts("This is free software: you are free to change and redistribute it.");
81         puts("There is NO WARRANTY, to the extent permitted by law.");
82 }
83
84 static void print_usage(FILE *f)
85 {
86         fprintf(f, "Usage: %s [options]\n", progname);
87         if (f != stdout)
88                 fprintf(f, "Try %s --help for more information.\n", progname);
89 }
90
91 static void print_help(void)
92 {
93         struct lopt_help help = {0};
94         const struct option *opt;
95
96         print_usage(stdout);
97
98         putchar('\n');
99         puts("Options:");
100         for (opt = lopts; opt->name; opt++) {
101                 if (!lopt_get_help(opt, &help))
102                         continue;
103                 help_print_option(opt, help.arg, help.desc, 20);
104         }
105         putchar('\n');
106
107         printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
108 }
109
110 static void draw_tile(WINDOW **win, unsigned colour, unsigned selected,
111                       unsigned x, unsigned y, unsigned start_column)
112 {
113         WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
114         int w, h, attr, ch, bc = selected ? '#' : 0;
115
116         assert(colour < TILE_MAX);
117         attr = COLOR_PAIR(colour);
118         switch (colour) {
119         case TILE_RED:    ch = 'X'; break;
120         case TILE_ORANGE: ch = '|'; break;
121         case TILE_GREEN:  ch = '+'; break;
122         case TILE_YELLOW: ch = '~'; attr |= A_BOLD; break;
123         case TILE_BLUE:   ch = 'o'; attr |= A_BOLD; break;
124         case TILE_WHITE:  ch = '.'; attr |= A_BOLD; break;
125
126         case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(RR_COLOUR_CURSOR);
127         }
128
129         getmaxyx(border, h, w);
130         w = 2*(w+1)/2;
131
132         if (mvwin(border, 1+GAME_YPOS+h*y, start_column+w*x) == ERR)
133                 return;
134
135         if (colour != TILE_EMPTY) {
136                 mvderwin(fill, 1, 1);
137                 wbkgdset(fill, A_REVERSE|attr|ch);
138                 werase(fill);
139         } else {
140                 werase(border);
141         }
142
143         if (bc || colour) {
144                 wattrset(border, attr);
145                 wborder(border, bc, bc, bc, bc, bc, bc, bc, bc);
146         }
147
148         wnoutrefresh(border);
149 }
150
151 static int
152 redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column,
153             uint_fast32_t bit0, uint_fast32_t bit1, uint_fast32_t bit2,
154             unsigned selected)
155 {
156         uint_fast32_t pos = board_position(x, y);
157         unsigned char tile = 0;
158
159         if (bit0 & pos) tile |= 1;
160         if (bit1 & pos) tile |= 2;
161         if (bit2 & pos) tile |= 4;
162         assert(tile < TILE_MAX);
163
164         draw_tile(win, tile, selected, x, y, start_column);
165         return tile;
166 }
167
168 static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
169 {
170         int w, h, tr = 0, rs = 0, br = 0, bs = 0, bl = 0;
171         WINDOW *area = win[WINDOW_AREA];
172
173         getmaxyx(stdscr, h, w);
174
175         if (h <= GAME_YPOS+1) {
176                 bl = ACS_ULCORNER, br = ACS_URCORNER;
177         } else if (h <= 3*sz+GAME_YPOS+1) {
178                 bl = br = ACS_VLINE, bs = ' ';
179         }
180
181         if (w <= x+1) {
182                 tr = ACS_ULCORNER, br = ACS_LLCORNER;
183         } else if (w <= 6*sz+x+2) {
184                 tr = ACS_HLINE, rs = ' ';
185                 br = br ? ' ' : ACS_HLINE;
186         }
187
188         wborder(area, 0, rs, 0, bs, 0, tr, bl, br);
189         wnoutrefresh(area);
190 }
191
192 static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
193 {
194         uint_least32_t buf[3], *gp = state->board.game;
195         int i;
196
197         if (mask == -1)
198                 redraw_area_border(state->gamewin, 2, 5);
199
200         if (state->view_goal_on_game) {
201                 for (i = 0; i < 3; i++) {
202                         buf[i] = state->board.goal[i];
203                         buf[i] = (gp[i] & ~GOAL_MASK) | (buf[i] << GOAL_SHIFT);
204                 }
205                 gp = buf;
206         }
207
208         for (i = 0; i < 25; i++) {
209                 if (mask & 1) {
210                         redraw_tile(state->gamewin, i%5, i/5,
211                                     4, gp[0], gp[1], gp[2],
212                                     i == state->cursor);
213                 }
214                 mask >>= 1;
215         }
216 }
217
218 static void curs_redraw_goal(struct app_state *state, uint_fast32_t mask)
219 {
220         uint_least16_t *gp = state->board.goal;
221         int i, x, y;
222
223         if (!state->goalwin[WINDOW_AREA])
224                 return;
225
226         getbegyx(state->goalwin[WINDOW_AREA], y, x);
227         if (mask == -1)
228                 redraw_area_border(state->goalwin, x, 3);
229
230         for (i = 0; i < 9; i++) {
231                 if (mask & 1) {
232                         redraw_tile(state->goalwin, i%3, i/3,
233                                     x+2, gp[0], gp[1], gp[2], 0);
234                 }
235                 mask >>= 1;
236         }
237 }
238
239 static WINDOW *realloc_area(WINDOW **orig, int h, int w, int y, int x)
240 {
241         WINDOW *win = *orig;
242
243         if (win) {
244 #if HAVE_CURSES_WRESIZE
245                 if (wresize(win, h, w) != ERR) {
246                         mvwin(win, y, x);
247                         return win;
248                 }
249 #endif
250                 delwin(win);
251         }
252
253         if (w > 0 && h > 0)
254                 return *orig = subwin(stdscr, h, w, y, x);
255         return *orig = NULL;
256 }
257
258 static void realloc_tiles(WINDOW **win, int h)
259 {
260         WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
261         int w = 2*h - 1;
262
263         if (fill && border) {
264                 int old_w, old_h;
265
266 #if HAVE_CURSES_WRESIZE
267                 if (wresize(fill, h-2, w-2) != ERR
268                     && wresize(border, h, w) != ERR)
269                 {
270                         return;
271                 }
272 #endif
273
274                 getmaxyx(border, old_h, old_w);
275                 if (old_h == h)
276                         return;
277         }
278
279         if (fill)
280                 delwin(fill);
281         if (border)
282                 delwin(border);
283
284         win[WINDOW_TILEBORDER] = border = newwin(h, w, 0, 0);
285         win[WINDOW_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
286 }
287
288 /*
289  * Given the toolbar function number (between 1 and 10, inclusive), and the
290  * total width of the screen, return the character position for the start of
291  * its label display.
292  *
293  * The intention is to divide the width of the screen into 10 roughly
294  * equally-sized areas, spreading out the remainder so that the width of
295  * each label is a monotone increasing function of the total width.
296  *
297  * The minimum size of a label is 6 characters (2 of which are used for the
298  * number indicator)
299  */
300 static int toolbar_xpos(int i, int total_width)
301 {
302         int button_width = MAX(6, total_width/10);
303         int rem = total_width - 10*button_width;
304         int pos = (i-1)*button_width;
305
306         switch (rem) {
307         case 9: pos += i > 6;
308         case 8: pos += i > 2;
309         case 7: pos += i > 7;
310         case 6: pos += i > 3;
311         case 5: pos += i > 8;
312         case 4: pos += i > 4;
313         case 3: pos += i > 9;
314         case 2: pos += i > 5;
315         }
316
317         return pos;
318 }
319
320 static void draw_toolbar(struct app_state *state)
321 {
322         WINDOW *toolbar = state->toolbar;
323         int i, w, lw;
324
325         getmaxyx(toolbar, i, w);
326         werase(toolbar);
327
328         lw = MAX(6, w/10);
329         mvwprintw(toolbar, 0, toolbar_xpos( 1, w)+2, "%.*s", lw, "Help");
330         mvwprintw(toolbar, 0, toolbar_xpos( 2, w)+2, "%.*s", lw, "NewGame");
331         mvwprintw(toolbar, 0, toolbar_xpos(10, w)+2, "%.*s", lw, "Exit");
332
333         mvwchgat(toolbar, 0, 0, -1, A_REVERSE, RR_COLOUR_TOOLBAR, 0);
334         for (i = 1; i <= 10; i++)
335                 mvwprintw(toolbar, 0, toolbar_xpos(i, w), "%2d", i);
336
337         wnoutrefresh(state->toolbar);
338 }
339
340 static void setup_mainwin(struct app_state *state)
341 {
342         int w, h, gamesz, goalsz, scr_w, scr_h, split;
343
344         getmaxyx(stdscr, scr_h, scr_w);
345
346         /* First try to fit the game tiles based on window height. */
347         gamesz = MAX(3, (scr_h - 4) / 5);
348
349         /* Adjust downward until we can fit smallest possible goal area. */
350         for (; split = 5+10*gamesz, gamesz > 3; gamesz--) {
351                 if (split + 20 < scr_w)
352                         break;
353         }
354
355         /* Pick a goal size that will fit in the remaining area */
356         goalsz = MAX(3, (scr_w - split - 4) / 6);
357         if (goalsz >= gamesz)
358                 goalsz = MAX(3, gamesz - 1);
359
360         realloc_tiles(state->gamewin, gamesz);
361         realloc_tiles(state->goalwin, goalsz);
362
363         /* Frame for game area */
364         w = MIN(scr_w-2, 3+10*gamesz);
365         h = MIN(scr_h-GAME_YPOS, 2+5*gamesz);
366         realloc_area(&state->gamewin[WINDOW_AREA], h, w, GAME_YPOS, 2);
367
368         /* Frame for goal area */
369         w = MIN(scr_w-split, 3+6*goalsz);
370         h = MIN(scr_h-GAME_YPOS, 2+3*goalsz);
371         realloc_area(&state->goalwin[WINDOW_AREA], h, w, GAME_YPOS, split);
372
373         /* Status area */
374         w = MAX(0, scr_w-split-1);
375         realloc_area(&state->timer, 1, w, GAME_YPOS+h, split+1);
376
377         /* Toolbar */
378         realloc_area(&state->toolbar, 1, scr_w, scr_h-1, 0);
379         draw_toolbar(state);
380 }
381
382 static void app_initialize(int argc, char **argv)
383 {
384         int enable_mouse = 1;
385         int opt;
386
387         if (argc > 0)
388                 progname = argv[0];
389
390         while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) {
391                 switch (opt) {
392                 case LOPT_MOUSE:
393                         enable_mouse = 2;
394                         break;
395                 case LOPT_NO_MOUSE:
396                         enable_mouse = 0;
397                         break;
398                 case LOPT_VERSION:
399                         print_version();
400                         exit(EXIT_SUCCESS);
401                 case LOPT_HELP:
402                         print_help();
403                         exit(EXIT_SUCCESS);
404                 default:
405                         print_usage(stderr);
406                         exit(EXIT_FAILURE);
407                 }
408         }
409
410         game_reset(&state.board);
411         state.cursor = -1;
412
413         initscr();
414         start_color();
415         curs_set(0);
416
417         cbreak();
418         keypad(stdscr, TRUE);
419         if (enable_mouse) {
420                 /*
421                  * While we only care about a subset of these events, for
422                  * some reason with ncurses failing to enable all of them
423                  * causes timeout to stop working when disabled buttons are
424                  * pressed (or released).
425                  *
426                  * It is not known if other curses have this problem; at least
427                  * pdcurses does not, but the extra events should be harmless
428                  * in any case.
429                  */
430 #if HAVE_CURSES_MOUSE_SUPPORT
431                 unsigned long mask = BUTTON1_PRESSED | BUTTON1_RELEASED
432                                    | BUTTON2_PRESSED | BUTTON2_RELEASED
433                                    | BUTTON3_PRESSED | BUTTON3_RELEASED
434 #ifdef BUTTON4_PRESSED
435                                    | BUTTON4_PRESSED | BUTTON4_RELEASED
436 #endif
437 #ifdef BUTTON5_PRESSED
438                                    | BUTTON5_PRESSED | BUTTON5_RELEASED
439 #endif
440                                    ;
441 #if HAVE_CURSES_MOUSE_SET
442                 mouse_set(mask);
443 #elif HAVE_CURSES_MOUSEMASK
444                 mousemask(mask, NULL);
445 #endif
446 #if HAVE_CURSES_MOUSEINTERVAL
447                 mouseinterval(0);
448 #endif
449 #endif /* HAVE_CURSES_MOUSE_SUPPORT */
450         }
451
452         noecho();
453
454         init_pair(TILE_RED, COLOR_RED, COLOR_BLACK);
455         init_pair(TILE_ORANGE, COLOR_YELLOW, COLOR_BLACK);
456         init_pair(TILE_YELLOW, COLOR_YELLOW, COLOR_BLACK);
457         init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
458         init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
459         init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
460         init_pair(RR_COLOUR_CURSOR, COLOR_BLACK, COLOR_BLACK);
461         init_pair(RR_COLOUR_TOOLBAR, COLOR_CYAN, COLOR_BLACK);
462
463         setup_mainwin(&state);
464         refresh();
465 }
466
467 static void update_timer(struct app_state *state, uint_fast32_t ms)
468 {
469         unsigned sec, min;
470
471         state->timer_ms = ms;
472         sec = ms / 1000; ms %= 1000;
473         min = sec / 60; sec %= 60;
474         mvwprintw(state->timer, 0, 0, "Time: %u:%.2u.%.3u",
475                   min, sec, (unsigned)ms);
476         wclrtoeol(state->timer);
477         wrefresh(state->timer);
478 }
479
480 static uint_fast32_t do_move(struct app_state *state, int x, int y)
481 {
482         uint_fast32_t mask;
483
484         if ((mask = game_do_move(&state->board, x, y)) != 0) {
485                 uint_fast32_t goal = game_check_goal(&state->board);
486
487                 if (state->view_goal_on_game) {
488                         state->view_goal_on_game = 0;
489                         mask |= goal;
490                 }
491
492                 if (goal == 0) {
493                         /* Solved! */
494                         update_timer(state, game_finish(&state->board));
495                         mask |= ~GOAL_MASK;
496                         state->cursor = -1;
497                         timeout(-1);
498                 }
499
500                 curs_redraw_game(state, mask);
501                 doupdate();
502         }
503
504         return mask;
505 }
506
507 static void do_reset_cursor(struct app_state *state)
508 {
509         state->cursor = 5*state->board.y + state->board.x;
510 }
511
512 static void do_new_game(struct app_state *state)
513 {
514         game_reset(&state->board);
515
516         do_reset_cursor(state);
517         curs_redraw_game(state, -1);
518         curs_redraw_goal(state, -1);
519         update_timer(state, 0);
520         doupdate();
521
522         timeout(33);
523         game_begin(&state->board);
524 }
525
526 static void do_function(struct app_state *state, unsigned func)
527 {
528         switch (func) {
529         case 2:
530                 do_new_game(state);
531                 break;
532         case 10:
533                 endwin();
534                 exit(0);
535         }
536 }
537
538 #if HAVE_CURSES_MOUSE_SUPPORT
539 /*
540  * Returns the toolbar function (1 through 10) under the given x, y screen
541  * coordinates, or 0 if the coordinates are outside of the toolbar.
542  */
543 static int mouse_toolbar_function(struct app_state *state, int x, int y)
544 {
545         int toolbar_x, toolbar_y, toolbar_w, toolbar_h, i;
546
547         getbegyx(state->toolbar, toolbar_y, toolbar_x);
548         getmaxyx(state->toolbar, toolbar_h, toolbar_w);
549
550         if ((void)toolbar_x, y != toolbar_y)
551                 return 0;
552
553         /* OK, selected a button, determine which one */
554         for (i = 10; i > 1; i--) {
555                 if ((void)toolbar_h, x >= toolbar_xpos(i, toolbar_w))
556                         break;
557         }
558
559         return i;
560 }
561
562 /*
563  * Given x, y as screen coordinates, record which (if any) toolbar function
564  * label is at that position, to be performed later.
565  *
566  * If no function is indicated, returns zero.  Otherwise, returns non-zero.
567  */
568 static int press_toolbar(struct app_state *state, int x, int y)
569 {
570         return state->toolbar_click = mouse_toolbar_function(state, x, y);
571 }
572
573 /*
574  * Perform the action previously recorded by press_toolbar, if and only if
575  * the x, y screen coordinates correspond to the same function.
576  */
577 static void release_toolbar(struct app_state *state, int x, int y)
578 {
579         int func = mouse_toolbar_function(state, x, y);
580
581         if (func && state->toolbar_click == func) {
582                 do_function(state, func);
583         }
584
585         state->toolbar_click = 0;
586 }
587
588 /*
589  * Given x, y as screen coordinates, determine which (if any) game tile is
590  * at that position.
591  *
592  * If there is no such tile, performs no action and returns 0.
593  *
594  * Otherwise, attempts to move that tile and returns non-zero.
595  */
596 static int press_tile(struct app_state *state, int x, int y)
597 {
598         int game_x, game_y, tile_w, tile_h;
599         uint_fast32_t cursor_mask, move_mask;
600
601         getbegyx(state->gamewin[WINDOW_AREA], game_y, game_x);
602         getmaxyx(state->gamewin[WINDOW_TILEBORDER], tile_h, tile_w);
603         tile_w += tile_w & 1;
604
605         /* special case the left spacer column */
606         if (x == game_x+1)
607                 x++;
608
609         if (x < game_x+2 || (x -= game_x+2)/5 >= tile_w)
610                 return 0;
611         if (y < game_y+1 || (y -= game_y+1)/5 >= tile_h)
612                 return 0;
613
614         /* OK, selected a tile. */
615         x /= tile_w;
616         y /= tile_h;
617
618         /* Disable the keyboard cursor due to mouse action */
619         cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor;
620         state->cursor = -1;
621
622         move_mask = do_move(state, x, y);
623         if ((cursor_mask & move_mask) == 0) {
624                 curs_redraw_game(state, cursor_mask);
625                 doupdate();
626         }
627
628         return 1;
629 }
630
631 static void do_mouse(struct app_state *state)
632 {
633         unsigned long bstate;
634         int x, y;
635
636 #if HAVE_CURSES_GETMOUSE_NCURSES
637         MEVENT mev;
638
639         if (getmouse(&mev) == ERR)
640                 return;
641
642         x = mev.x, y = mev.y;
643         bstate = mev.bstate;
644 #elif HAVE_CURSES_REQUEST_MOUSE_POS
645         request_mouse_pos();
646         x = MOUSE_X_POS;
647         y = MOUSE_Y_POS;
648
649         bstate = 0;
650
651 #define set_bstate_helper(button) do { if (BUTTON_CHANGED(button)) \
652         switch (BUTTON_STATUS(button)) { \
653         case BUTTON_RELEASED: bstate |= BUTTON ## button ##  _RELEASED; break; \
654         case BUTTON_PRESSED:  bstate |= BUTTON ## button ##  _PRESSED; break; \
655         } \
656 } while (0);
657
658         set_bstate_helper(1);
659         set_bstate_helper(3);
660 #endif
661         if (bstate & BUTTON3_PRESSED) {
662                 state->view_goal_on_game = 1;
663                 curs_redraw_game(state, game_check_goal(&state->board));
664                 doupdate();
665         }
666
667         if (bstate & BUTTON3_RELEASED) {
668                 state->view_goal_on_game = 0;
669                 curs_redraw_game(state, game_check_goal(&state->board));
670                 doupdate();
671         }
672
673         /* Ignore button1 if holding button3 to view goal */
674         if (state->view_goal_on_game & 1)
675                 return;
676
677         if (bstate & BUTTON1_PRESSED) {
678                 if (press_toolbar(state, x, y));
679                 else if (press_tile(state, x, y));
680         }
681
682         if (bstate & BUTTON1_RELEASED) {
683                 release_toolbar(state, x, y);
684         }
685 }
686 #endif /* HAVE_CURSES_MOUSE_SUPPORT */
687
688 static void do_move_cursor(struct app_state *state, int c)
689 {
690         uint_fast32_t mask = 0;
691
692         if (state->cursor < 0) {
693                 /* Cursor was hidden; reset it */
694                 do_reset_cursor(state);
695         } else {
696                 mask = 1ul << state->cursor;
697         }
698
699         if (state->view_goal_on_game) {
700                 state->view_goal_on_game = 0;
701                 mask |= game_check_goal(&state->board);
702         }
703
704         switch (c) {
705         case KEY_UP:
706                 if ((state->cursor -= 5) < 0)
707                         state->cursor += 25;
708                 break;
709         case KEY_DOWN:
710                 if ((state->cursor += 5) >= 25)
711                         state->cursor -= 25;
712                 break;
713         case KEY_LEFT:
714                 if ((state->cursor -= 1) % 5 == 4 || state->cursor < 0)
715                         state->cursor += 5;
716                 break;
717         case KEY_RIGHT:
718                 if ((state->cursor += 1) % 5 == 0)
719                         state->cursor -= 5;
720                 break;
721         }
722
723         curs_redraw_game(state, mask | 1ul << state->cursor);
724         doupdate();
725 }
726
727 static void do_keystroke(struct app_state *state, int c)
728 {
729         int last_input = state->last_input;
730         state->last_input = c;
731
732         switch (c) {
733         case KEY_DOWN: case KEY_UP: case KEY_LEFT: case KEY_RIGHT:
734                 do_move_cursor(state, c);
735                 break;
736         case '\t':
737                 state->view_goal_on_game ^= 2u;
738                 curs_redraw_game(state, game_check_goal(&state->board));
739                 doupdate();
740                 break;
741         case ' ':
742                 if (!(state->view_goal_on_game & 1) && state->cursor >= 0)
743                         do_move(state, state->cursor%5, state->cursor/5);
744                 break;
745         }
746
747         /* ESC+# keys */
748         if (last_input == '\33' && c >= '0' && c <= '9') {
749                 do_function(state, (c -= '0') == 0 ? 10 : c);
750         }
751
752         /* F# keys */
753         if (c >= KEY_F(1) && c <= KEY_F(10)) {
754                 do_function(state, c - KEY_F0);
755         }
756 }
757
758 /* One iteration of main input loop */
759 void do_mainloop(struct app_state *state)
760 {
761         int c = getch();
762
763         switch (c) {
764 #ifdef KEY_RESIZE
765         case KEY_RESIZE:
766                 setup_mainwin(state);
767                 clear();
768                 refresh();
769                 curs_redraw_game(state, -1);
770                 curs_redraw_goal(state, -1);
771                 draw_toolbar(state);
772                 update_timer(state, state->timer_ms);
773                 break;
774 #endif
775 #if HAVE_CURSES_MOUSE_SUPPORT
776         case KEY_MOUSE:
777                 do_mouse(state);
778                 break;
779 #endif
780         default:
781                 do_keystroke(state, c);
782         case ERR:;
783         }
784
785         if (state->board.x <= 4)
786                 update_timer(state, game_elapsed(&state->board));
787 }
788
789 int main(int argc, char **argv)
790 {
791         setlocale(LC_ALL, "");
792         app_initialize(argc, argv);
793
794         do_new_game(&state);
795         while (1)
796                 do_mainloop(&state);
797         abort();
798 }