#include "help.h"
#include "version.h"
-#include "cursesopt.h"
-#include "game.h"
-#define MIN(a, b) ((a) < (b) ? (a) : (b))
-#define MAX(a, b) ((a) > (b) ? (a) : (b))
+#include "cursesui.h"
+#include "cursesopt.h"
enum {
GAME_YPOS = 1 // top row of game and goal areas.
static const char *progname = "rrace";
static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
-enum {
- WINDOW_TILEBORDER,
- WINDOW_TILEFILL,
- WINDOW_AREA,
- WINDOW_MAX,
-};
-
-/* Colour pair enumeration */
-enum {
- /* Pairs 1-6 correspond to tile colours */
- RR_COLOUR_CURSOR = TILE_MAX, // black on black, for the cursor
- RR_COLOUR_TOOLBAR, // cyan on black (use reverse video)
- RR_COLOUR_MAX
-};
-
-static struct app_state {
- struct board board;
-
- WINDOW *gamewin[WINDOW_MAX], *goalwin[WINDOW_MAX];
- WINDOW *toolbar, *timer;
- int last_input;
-
- /* Most recently displayed timer value, for screen redraw. */
- uint_least32_t timer_ms;
-
- /* Location of the keyboard cursor */
- int_least8_t cursor;
-
- /* If true, the goal will be displayed over the main play area. */
- uint_least8_t view_goal_on_game;
-
- /* Clicked toolbar item */
- uint_least8_t toolbar_click;
-} state;
+static struct app_state state;
static void print_version(void)
{
static void draw_tile(WINDOW **win, unsigned colour, unsigned selected,
unsigned x, unsigned y, unsigned start_column)
{
- WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
+ WINDOW *border = win[PLAYWIN_TILEBORDER], *fill = win[PLAYWIN_TILEFILL];
int w, h, attr, ch, bc = selected ? '#' : 0;
assert(colour < TILE_MAX);
case TILE_BLUE: ch = 'o'; attr |= A_BOLD; break;
case TILE_WHITE: ch = '.'; attr |= A_BOLD; break;
- case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(RR_COLOUR_CURSOR);
+ case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(RR_COLOUR_SHADOW);
}
getmaxyx(border, h, w);
static void redraw_area_border(WINDOW **win, unsigned x, unsigned sz)
{
int w, h, tr = 0, rs = 0, br = 0, bs = 0, bl = 0;
- WINDOW *area = win[WINDOW_AREA];
+ WINDOW *area = win[PLAYWIN_AREA];
getmaxyx(stdscr, h, w);
uint_least16_t *gp = state->board.goal;
int i, x, y;
- if (!state->goalwin[WINDOW_AREA])
+ if (!state->goalwin[PLAYWIN_AREA])
return;
- getbegyx(state->goalwin[WINDOW_AREA], y, x);
+ getbegyx(state->goalwin[PLAYWIN_AREA], y, x);
if (mask == -1)
redraw_area_border(state->goalwin, x, 3);
static void realloc_tiles(WINDOW **win, int h)
{
- WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
+ WINDOW *border = win[PLAYWIN_TILEBORDER], *fill = win[PLAYWIN_TILEFILL];
int w = 2*h - 1;
if (fill && border) {
if (border)
delwin(border);
- win[WINDOW_TILEBORDER] = border = newwin(h, w, 0, 0);
- win[WINDOW_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
-}
-
-/*
- * Given the toolbar function number (between 1 and 10, inclusive), and the
- * total width of the screen, return the character position for the start of
- * its label display.
- *
- * The intention is to divide the width of the screen into 10 roughly
- * equally-sized areas, spreading out the remainder so that the width of
- * each label is a monotone increasing function of the total width.
- *
- * The minimum size of a label is 6 characters (2 of which are used for the
- * number indicator)
- */
-static int toolbar_xpos(int i, int total_width)
-{
- int button_width = MAX(6, total_width/10);
- int rem = total_width - 10*button_width;
- int pos = (i-1)*button_width;
-
- switch (rem) {
- case 9: pos += i > 6;
- case 8: pos += i > 2;
- case 7: pos += i > 7;
- case 6: pos += i > 3;
- case 5: pos += i > 8;
- case 4: pos += i > 4;
- case 3: pos += i > 9;
- case 2: pos += i > 5;
- }
-
- return pos;
-}
-
-static void draw_toolbar(struct app_state *state)
-{
- WINDOW *toolbar = state->toolbar;
- int i, w, lw;
-
- getmaxyx(toolbar, i, w);
- werase(toolbar);
-
- lw = MAX(6, w/10);
- mvwprintw(toolbar, 0, toolbar_xpos( 1, w)+2, "%.*s", lw, "Help");
- mvwprintw(toolbar, 0, toolbar_xpos( 2, w)+2, "%.*s", lw, "NewGame");
- mvwprintw(toolbar, 0, toolbar_xpos(10, w)+2, "%.*s", lw, "Exit");
-
- mvwchgat(toolbar, 0, 0, -1, A_REVERSE, RR_COLOUR_TOOLBAR, 0);
- for (i = 1; i <= 10; i++)
- mvwprintw(toolbar, 0, toolbar_xpos(i, w), "%2d", i);
-
- wnoutrefresh(state->toolbar);
+ win[PLAYWIN_TILEBORDER] = border = newwin(h, w, 0, 0);
+ win[PLAYWIN_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
}
static void setup_mainwin(struct app_state *state)
/* Frame for game area */
w = MIN(scr_w-2, 3+10*gamesz);
h = MIN(scr_h-GAME_YPOS, 2+5*gamesz);
- realloc_area(&state->gamewin[WINDOW_AREA], h, w, GAME_YPOS, 2);
+ realloc_area(&state->gamewin[PLAYWIN_AREA], h, w, GAME_YPOS, 2);
/* Frame for goal area */
w = MIN(scr_w-split, 3+6*goalsz);
h = MIN(scr_h-GAME_YPOS, 2+3*goalsz);
- realloc_area(&state->goalwin[WINDOW_AREA], h, w, GAME_YPOS, split);
+ realloc_area(&state->goalwin[PLAYWIN_AREA], h, w, GAME_YPOS, split);
/* Status area */
w = MAX(0, scr_w-split-1);
/* Toolbar */
realloc_area(&state->toolbar, 1, scr_w, scr_h-1, 0);
- draw_toolbar(state);
+ curs_draw_toolbar(state);
}
static void app_initialize(int argc, char **argv)
init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
- init_pair(RR_COLOUR_CURSOR, COLOR_BLACK, COLOR_BLACK);
+ init_pair(RR_COLOUR_SHADOW, COLOR_BLACK, COLOR_BLACK);
init_pair(RR_COLOUR_TOOLBAR, COLOR_CYAN, COLOR_BLACK);
setup_mainwin(&state);
mvwprintw(state->timer, 0, 0, "Time: %u:%.2u.%.3u",
min, sec, (unsigned)ms);
wclrtoeol(state->timer);
- wrefresh(state->timer);
+ wnoutrefresh(state->timer);
}
static uint_fast32_t do_move(struct app_state *state, int x, int y)
state->cursor = 5*state->board.y + state->board.x;
}
-static void do_new_game(struct app_state *state)
+void curs_new_game(struct app_state *state)
{
game_reset(&state->board);
game_begin(&state->board);
}
-static void do_function(struct app_state *state, unsigned func)
-{
- switch (func) {
- case 2:
- do_new_game(state);
- break;
- case 10:
- endwin();
- exit(0);
- }
-}
-
#if HAVE_CURSES_MOUSE_SUPPORT
-/*
- * Returns the toolbar function (1 through 10) under the given x, y screen
- * coordinates, or 0 if the coordinates are outside of the toolbar.
- */
-static int mouse_toolbar_function(struct app_state *state, int x, int y)
-{
- int toolbar_x, toolbar_y, toolbar_w, toolbar_h, i;
-
- getbegyx(state->toolbar, toolbar_y, toolbar_x);
- getmaxyx(state->toolbar, toolbar_h, toolbar_w);
-
- if ((void)toolbar_x, y != toolbar_y)
- return 0;
-
- /* OK, selected a button, determine which one */
- for (i = 10; i > 1; i--) {
- if ((void)toolbar_h, x >= toolbar_xpos(i, toolbar_w))
- break;
- }
-
- return i;
-}
-
/*
* Given x, y as screen coordinates, record which (if any) toolbar function
* label is at that position, to be performed later.
*/
static int press_toolbar(struct app_state *state, int x, int y)
{
- return state->toolbar_click = mouse_toolbar_function(state, x, y);
+ return state->toolbar_click = curs_toolbar_mouse_func(state, x, y);
}
/*
+ * Given x, y as screen coordinates, perform the toolbar action.
+ *
* Perform the action previously recorded by press_toolbar, if and only if
* the x, y screen coordinates correspond to the same function.
*/
static void release_toolbar(struct app_state *state, int x, int y)
{
- int func = mouse_toolbar_function(state, x, y);
+ int func = curs_toolbar_mouse_func(state, x, y);
if (func && state->toolbar_click == func) {
- do_function(state, func);
+ curs_execute_function(state, func);
}
state->toolbar_click = 0;
int game_x, game_y, tile_w, tile_h;
uint_fast32_t cursor_mask, move_mask;
- getbegyx(state->gamewin[WINDOW_AREA], game_y, game_x);
- getmaxyx(state->gamewin[WINDOW_TILEBORDER], tile_h, tile_w);
+ getbegyx(state->gamewin[PLAYWIN_AREA], game_y, game_x);
+ getmaxyx(state->gamewin[PLAYWIN_TILEBORDER], tile_h, tile_w);
tile_w += tile_w & 1;
/* special case the left spacer column */
/* ESC+# keys */
if (last_input == '\33' && c >= '0' && c <= '9') {
- do_function(state, (c -= '0') == 0 ? 10 : c);
+ curs_execute_function(state, (c -= '0') == 0 ? 10 : c);
}
/* F# keys */
if (c >= KEY_F(1) && c <= KEY_F(10)) {
- do_function(state, c - KEY_F0);
+ curs_execute_function(state, c - KEY_F0);
}
}
refresh();
curs_redraw_game(state, -1);
curs_redraw_goal(state, -1);
- draw_toolbar(state);
+ curs_draw_toolbar(state);
update_timer(state, state->timer_ms);
+ doupdate();
break;
#endif
#if HAVE_CURSES_MOUSE_SUPPORT
case ERR:;
}
- if (state->board.x <= 4)
+ if (state->board.x <= 4) {
update_timer(state, game_elapsed(&state->board));
+ doupdate();
+ }
}
int main(int argc, char **argv)
setlocale(LC_ALL, "");
app_initialize(argc, argv);
- do_new_game(&state);
+ curs_new_game(&state);
while (1)
do_mainloop(&state);
abort();
--- /dev/null
+/*
+ * Curses UI for slide puzzle game
+ * Copyright © 2022 Nick Bowler
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#ifndef RRACE_CURSESUI_H_
+#define RRACE_CURSESUI_H_
+
+#include "game.h"
+
+/* Array indices for gamewin and goalwin window sets. */
+enum {
+ PLAYWIN_TILEBORDER,
+ PLAYWIN_TILEFILL,
+ PLAYWIN_AREA,
+ PLAYWIN_MAX
+};
+
+/* Colour pair enumeration */
+enum {
+ /* Pairs 1-6 correspond to tile colours; use TILE_xxx enumerations. */
+ RR_COLOUR_SHADOW = TILE_MAX, /* black on black (use bold) */
+ RR_COLOUR_TOOLBAR, /* cyan on black (use reverse video) */
+ RR_COLOUR_MAX
+};
+
+struct app_state {
+ struct board board;
+
+ WINDOW *gamewin[PLAYWIN_MAX], *goalwin[PLAYWIN_MAX];
+ WINDOW *toolbar, *timer;
+
+ /* Previous input returned from getch, for 2-character sequences */
+ int last_input;
+
+ /* Most recently displayed timer value, for screen redraw. */
+ uint_least32_t timer_ms;
+
+ /* Location of the keyboard cursor */
+ int_least8_t cursor;
+
+ /* If true, the goal will be displayed over the main play area. */
+ uint_least8_t view_goal_on_game;
+
+ /* Clicked toolbar item */
+ uint_least8_t toolbar_click;
+
+ /* Current state of the toolbar menu */
+ uint_least8_t toolbar_state;
+};
+
+void curs_new_game(struct app_state *state);
+
+void curs_reenable_dialog(struct app_state *state, const char *heading);
+
+void curs_draw_toolbar(struct app_state *state);
+void curs_execute_function(struct app_state *state, int function);
+
+#if HAVE_CURSES_MOUSE_SUPPORT
+int curs_toolbar_mouse_func(struct app_state *state, int x, int y);
+#else
+static inline int
+curs_toolbar_mouse_func(struct app_state *state, int x, int y)
+{
+ return 0;
+}
+#endif
+
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+#define MAX(a, b) ((a) > (b) ? (a) : (b))
+
+#endif
--- /dev/null
+/*
+ * Curses UI for slide puzzle game
+ * Copyright © 2022 Nick Bowler
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdlib.h>
+#include <curses.h>
+
+#include "cursesui.h"
+#include "cursmenu.h"
+
+enum {
+ ACTION_EXIT = 0x80,
+ ACTION_ABOUT,
+ ACTION_NEWGAME,
+ ACTION_MAX
+};
+
+static const struct menuitem {
+ uint_least8_t label, position, action;
+} menu[] = { FUNC_INITIALIZER };
+
+static void do_function(struct app_state *state, unsigned action)
+{
+ if (action < sizeof menu / sizeof menu[0]) {
+ state->toolbar_state = action;
+ curs_draw_toolbar(state);
+ doupdate();
+ }
+
+ switch (action) {
+ case ACTION_NEWGAME:
+ curs_new_game(state);
+ break;
+ case ACTION_EXIT:
+ endwin();
+ exit(0);
+ };
+}
+
+void curs_execute_function(struct app_state *state, int func)
+{
+ int i;
+
+ for (i = state->toolbar_state; menu[i].position; i++) {
+ if (func == menu[i].position) {
+ do_function(state, menu[i].action);
+ return;
+ }
+ }
+}
+
+/*
+ * Return the width of the toolbar and the target width of toolbar labels.
+ */
+static int toolbar_width(WINDOW *toolbar, int *label_width)
+{
+ int w, h;
+
+ getmaxyx(toolbar, h, w);
+ *label_width = MAX(6, w/10);
+ return (void)h, w;
+}
+
+/*
+ * Given the toolbar function number (between 1 and 10, inclusive), and the
+ * total width of the screen, return the character position for the start of
+ * its label display.
+ *
+ * The intention is to divide the width of the screen into 10 roughly
+ * equally-sized areas, spreading out the remainder so that the width of
+ * each label is a monotone increasing function of the total width.
+ *
+ * The minimum size of a label is 6 characters (2 of which are used for the
+ * number indicator)
+ */
+static int toolbar_xpos(int i, int total_width, int label_width)
+{
+ int rem = total_width - 10*label_width;
+ int pos = (i-1)*label_width;
+
+ switch (rem) {
+ case 9: pos += i > 6;
+ case 8: pos += i > 2;
+ case 7: pos += i > 7;
+ case 6: pos += i > 3;
+ case 5: pos += i > 8;
+ case 4: pos += i > 4;
+ case 3: pos += i > 9;
+ case 2: pos += i > 5;
+ }
+
+ return pos;
+}
+
+/*
+ * Redraw the current set of toolbar labels.
+ */
+void curs_draw_toolbar(struct app_state *state)
+{
+ WINDOW *toolbar = state->toolbar;
+ int i, w, lw;
+
+ werase(toolbar);
+
+ w = toolbar_width(toolbar, &lw);
+ for (i = state->toolbar_state; menu[i].position; i++) {
+ int pos = toolbar_xpos(menu[i].position, w, lw);
+ mvwprintw(toolbar, 0, pos+2, "%.*s", lw, strtab+menu[i].label);
+ }
+
+ mvwchgat(toolbar, 0, 0, -1, A_REVERSE, RR_COLOUR_TOOLBAR, 0);
+ for (i = 1; i <= 10; i++) {
+ mvwprintw(toolbar, 0, toolbar_xpos(i, w, lw), "%2d", i);
+ }
+
+ wnoutrefresh(state->toolbar);
+}
+
+#if HAVE_CURSES_MOUSE_SUPPORT
+/*
+ * Returns the toolbar function (1 through 10) under the given x, y screen
+ * coordinates, or 0 if the coordinates are outside of the toolbar.
+ */
+int curs_toolbar_mouse_func(struct app_state *state, int x, int y)
+{
+ int toolbar_x, toolbar_y, i, w, lw;
+
+ getbegyx(state->toolbar, toolbar_y, toolbar_x);
+ if ((void)toolbar_x, y != toolbar_y)
+ return 0;
+
+ /* OK, selected a button, determine which one */
+ w = toolbar_width(state->toolbar, &lw);
+ for (i = 10; i > 1; i--) {
+ if (x >= toolbar_xpos(i, w, lw))
+ break;
+ }
+
+ return i;
+}
+#endif