From 7c0e8a953d0057ec04f5d77c713a34c1ab30a7d6 Mon Sep 17 00:00:00 2001 From: Nick Bowler Date: Sun, 5 Jun 2022 16:42:45 -0400 Subject: [PATCH] Add initial curses-based UI. To start getting an idea of what it takes to implement a game like this with many different frontends, let's start by implementing a text-mode user interface based on curses. --- .gitignore | 1 + Makefile.am | 24 +++-- common | 2 +- configure.ac | 12 +++ src/.gitignore | 1 + src/curses.c | 263 ++++++++++++++++++++++++++++++++++++++++++++++ src/cursesopt.opt | 5 + 7 files changed, 301 insertions(+), 7 deletions(-) create mode 100644 src/curses.c create mode 100644 src/cursesopt.opt diff --git a/.gitignore b/.gitignore index fc36602..c3b122a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ /ltmain.sh /missing /package.m4 +/rrace-curses /rrace-motif /stamp-h1 /testsuite diff --git a/Makefile.am b/Makefile.am index 887411a..0334b20 100644 --- a/Makefile.am +++ b/Makefile.am @@ -12,27 +12,39 @@ MAINTAINERCLEANFILES = MOSTLYCLEANFILES = DISTCLEANFILES = CLEANFILES = $(EXTRA_LIBRARIES) +bin_PROGRAMS = AM_CPPFLAGS = -I$(builddir)/src -I$(srcdir)/src -I$(DX_BASEDIR)/src \ -I$(builddir)/lib -I$(srcdir)/lib -AM_CFLAGS = $(MOTIF_CFLAGS) +AM_CFLAGS = $(CURSES_CFLAGS) $(MOTIF_CFLAGS) + +if HAVE_CURSES +bin_PROGRAMS += rrace-curses +endif if HAVE_MOTIF -bin_PROGRAMS = rrace-motif +bin_PROGRAMS += rrace-motif dist_man_MANS = doc/rrace-motif.1 endif -noinst_HEADERS = conf_post.h +noinst_HEADERS = conf_post.h src/version.h + +rrace_curses_SOURCES = common/src/help.c src/game.c src/version.c +rrace_curses_LDADD = $(libcursesmain_a_OBJECTS) libgnu.a $(CURSES_LIBS) rrace_motif_SOURCES = src/game.c src/x11.c src/game.h src/motif.h \ src/colour.h src/ewmhicon.c src/ewmhicon.h \ - src/version.c src/version.h src/xcounter.c \ - src/xcounter.h + src/version.c src/xcounter.c src/xcounter.h rrace_motif_LDADD = $(libmotifmain_a_OBJECTS) $(libmotifui_a_OBJECTS) \ $(libglohelp_a_OBJECTS) libgnu.a $(MOTIF_LIBS) \ $(LIB_CLOCK_GETTIME) $(LIB_GETHRXTIME) $(rrace_motif_OBJECTS): $(gnulib_headers) +EXTRA_LIBRARIES += libcursesmain.a +libcursesmain_a_SOURCES = src/curses.c +$(libcursesmain_a_OBJECTS): $(gnulib_headers) +$(libcursesmain_a_OBJECTS): src/cursesopt.h + EXTRA_LIBRARIES += libmotifmain.a libmotifmain_a_SOURCES = src/motif.c $(libmotifmain_a_OBJECTS): $(gnulib_headers) @@ -49,7 +61,7 @@ libglohelp_a_CFLAGS = -DHELP_GETOPT_LONG_ONLY $(libglohelp_a_OBJECTS): $(gnulib_headers) libglohelp_a_SHORTNAME = glo -OPTFILES = src/motifopt.opt +OPTFILES = src/cursesopt.opt src/motifopt.opt .opt.h: $(AM_V_GEN) $(AWK) -f $(DX_BASEDIR)/scripts/gen-options.awk $< >$@.tmp $(AM_V_at) mv -f $@.tmp $@ diff --git a/common b/common index a7cabb5..18c520e 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit a7cabb5d0f067e78afd029d8ec41d14660d8f9e2 +Subproject commit 18c520e1c6d44a8c68328f3ffa4d64434fb454b8 diff --git a/configure.ac b/configure.ac index 3709434..6be037e 100644 --- a/configure.ac +++ b/configure.ac @@ -29,6 +29,17 @@ m4_traceoff([AM_GNU_GETTEXT]) AM_GNU_GETTEXT([external]) AH_BOTTOM([#include ]) +# Checks for curses +AC_ARG_WITH([curses], [AS_HELP_STRING([--with-curses], + [use curses for playing in text mode (default: auto)])], + [], [with_curses=auto]) +AS_IF([test x"$with_curses" != x"no"], + [DX_LIB_CURSES([have_curses=yes], [have_curses=no])]) +AS_IF([test x"$with_curses" = x"yes" && x"$have_curses" != x"yes"], + [AC_MSG_FAILURE([--with-curses requested but curses was not found])]) +AM_CONDITIONAL([HAVE_CURSES], [test x"$have_curses" = x"yes"]) + +# Checks for X11 AC_PATH_XTRA AS_IF([test x"$no_x" != x"yes"], [AC_CACHE_CHECK([for Motif], [dx_cv_have_motif], @@ -112,6 +123,7 @@ AC_CONFIG_FILES([Makefile]) AC_OUTPUT have_ui=false +AM_COND_IF([HAVE_CURSES], [have_ui=:]) AM_COND_IF([HAVE_MOTIF], [have_ui=:]) AS_IF([$have_ui], [], [AC_MSG_WARN([No user interface is enabled.]) diff --git a/src/.gitignore b/src/.gitignore index 97c21ad..977700c 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1,3 +1,4 @@ +/cursesopt.h /motifgui.h /motifopt.h /motifstr.h diff --git a/src/curses.c b/src/curses.c new file mode 100644 index 0000000..bf97caf --- /dev/null +++ b/src/curses.c @@ -0,0 +1,263 @@ +/* + * 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 . + */ + +#include +#include +#include +#include +#include +#include + +#include "help.h" +#include "version.h" +#include "cursesopt.h" +#include "game.h" + +static const char *progname = "rrace"; +static const struct option lopts[] = { LOPTS_INITIALIZER, {0} }; + +static struct app_state { + struct board board; + + WINDOW *tile_border, *tile_fill; +} state; + +static void print_version(void) +{ + version_print_head("rrace-curses", stdout); + puts("License GPLv3+: GNU GPL version 3 or any later version"); + puts("This is free software: you are free to change and redistribute it."); + puts("There is NO WARRANTY, to the extent permitted by law."); +} + +static void print_usage(FILE *f) +{ + fprintf(f, "Usage: %s [options]\n", progname); + if (f != stdout) + fprintf(f, "Try %s --help for more information.\n", progname); +} + +static void print_help(void) +{ + struct lopt_help help = {0}; + const struct option *opt; + + print_usage(stdout); + + putchar('\n'); + puts("Options:"); + for (opt = lopts; opt->name; opt++) { + if (!lopt_get_help(opt, &help)) + continue; + help_print_option(opt, help.arg, help.desc, 20); + } + putchar('\n'); + + printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT); +} + +static void +draw_tile(struct app_state *state, unsigned colour, unsigned x, unsigned y) +{ + int attr, ch; + int w, h; + + assert(colour < TILE_MAX); + attr = COLOR_PAIR(colour); + switch (colour) { + case TILE_RED: ch = 'X'; break; + case TILE_ORANGE: ch = '|'; break; + case TILE_GREEN: ch = '+'; break; + case TILE_YELLOW: ch = '~'; attr |= A_BOLD; break; + case TILE_BLUE: ch = 'o'; attr |= A_BOLD; break; + case TILE_WHITE: ch = '.'; attr |= A_BOLD; break; + } + + getmaxyx(state->tile_border, h, w); + w = 2*(w+1)/2; + + if (mvwin(state->tile_border, 2+h*y, 4+w*x) == ERR) + return; + + if (colour != TILE_EMPTY) { + wattrset(state->tile_border, attr); + box(state->tile_border, 0, 0); + + mvderwin(state->tile_fill, 1, 1); + wbkgdset(state->tile_fill, A_REVERSE|attr|ch); + werase(state->tile_fill); + } else { + werase(state->tile_border); + } + + wnoutrefresh(state->tile_border); +} + +static int curs_redraw_tile(struct app_state *state, unsigned x, unsigned y) +{ + uint_fast32_t pos = board_position(x, y); + unsigned char tile = 0; + + if (state->board.game[0] & pos) tile |= 1; + if (state->board.game[1] & pos) tile |= 2; + if (state->board.game[2] & pos) tile |= 4; + assert(tile < TILE_MAX); + + draw_tile(state, tile, x, y); + return tile; +} + +static void curs_redraw_game(struct app_state *state, uint_fast32_t mask) +{ + int i; + + for (i = 0; i < 25; i++) { + if (mask & 1) { + curs_redraw_tile(state, i%5, i/5); + } + mask >>= 1; + } +} + +static void curs_alloc_tiles(struct app_state *state) +{ + int w, h, tilesz; + WINDOW *tile; + + getmaxyx(stdscr, h, w); + tilesz = (h - 4) / 5; + if (tilesz < 3) + tilesz = 3; + + h = -1; + if (state->tile_border) { + getmaxyx(state->tile_border, h, w); + } + + if (h == tilesz) { + /* Nothing to do. */ + return; + } + + if (state->tile_border) { + delwin(state->tile_fill); + delwin(state->tile_border); + } + + state->tile_border = tile = newwin(tilesz, 2*tilesz-1, 0, 0); + state->tile_fill = derwin(tile, tilesz-2, 2*tilesz-3, 1, 1); +} + +static void app_initialize(int argc, char **argv) +{ + int opt; + + if (argc > 0) + progname = argv[0]; + + while ((opt = getopt_long(argc, argv, SOPT_STRING, lopts, 0)) != -1) { + switch (opt) { + case LOPT_VERSION: + print_version(); + exit(EXIT_SUCCESS); + case LOPT_HELP: + print_help(); + exit(EXIT_SUCCESS); + default: + print_usage(stderr); + exit(EXIT_FAILURE); + } + } + + game_reset(&state.board); + + initscr(); + start_color(); + if (curs_set(0) != ERR) + leaveok(stdscr, TRUE); + + cbreak(); + keypad(stdscr, TRUE); + mousemask(BUTTON1_PRESSED, NULL); + mouseinterval(0); + noecho(); + + init_pair(TILE_RED, COLOR_RED, COLOR_BLACK); + init_pair(TILE_ORANGE, COLOR_YELLOW, COLOR_BLACK); + init_pair(TILE_YELLOW, COLOR_YELLOW, COLOR_BLACK); + init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK); + init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK); + init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK); + + curs_alloc_tiles(&state); + refresh(); +} + +static void do_move(struct app_state *state, int x, int y) +{ + uint_fast32_t mask; + + if ((mask = game_do_move(&state->board, x, y)) != 0) { + curs_redraw_game(state, mask); + refresh(); + } +} + +static void do_mouse(struct app_state *state, MEVENT *mev) +{ + if (mev->bstate == BUTTON1_PRESSED) { + int w, h, x, y; + + /* Determine size of the game area */ + getmaxyx(state->tile_border, h, w); + w = 2*(w+1)/2; + + if (mev->x < 4 || (x = mev->x - 4)/5 >= w) return; + if (mev->y < 2 || (y = mev->y - 2)/5 >= h) return; + + do_move(state, x/w, y/h); + } +} + +int main(int argc, char **argv) +{ + setlocale(LC_ALL, ""); + app_initialize(argc, argv); + + curs_redraw_game(&state, -1); + refresh(); + + while (1) { + int c = getch(); + MEVENT mev; + + switch (c) { + case KEY_RESIZE: + curs_alloc_tiles(&state); + clear(); + refresh(); + curs_redraw_game(&state, -1); + refresh(); + break; + case KEY_MOUSE: + if (getmouse(&mev) != ERR) + do_mouse(&state, &mev); + break; + } + } +} diff --git a/src/cursesopt.opt b/src/cursesopt.opt new file mode 100644 index 0000000..fe0f942 --- /dev/null +++ b/src/cursesopt.opt @@ -0,0 +1,5 @@ +--version +Print a version message and then exit. + +--help +Print this message and then exit. -- 2.43.2