]> git.draconx.ca Git - rrace.git/commitdiff
Add initial curses-based UI.
authorNick Bowler <nbowler@draconx.ca>
Sun, 5 Jun 2022 20:42:45 +0000 (16:42 -0400)
committerNick Bowler <nbowler@draconx.ca>
Wed, 8 Jun 2022 02:30:19 +0000 (22:30 -0400)
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
Makefile.am
common
configure.ac
src/.gitignore
src/curses.c [new file with mode: 0644]
src/cursesopt.opt [new file with mode: 0644]

index fc3660259262d6e5333bb5155205ca1fe33a64ee..c3b122aaf938a5ff62ea6203b6f7020047a571b4 100644 (file)
@@ -18,6 +18,7 @@
 /ltmain.sh
 /missing
 /package.m4
+/rrace-curses
 /rrace-motif
 /stamp-h1
 /testsuite
index 887411a72c17f733024ff44edae6f59fed9a30af..0334b2049da3d6b1a58633b1c2fea25131a5a517 100644 (file)
@@ -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 a7cabb5d0f067e78afd029d8ec41d14660d8f9e2..18c520e1c6d44a8c68328f3ffa4d64434fb454b8 160000 (submodule)
--- a/common
+++ b/common
@@ -1 +1 @@
-Subproject commit a7cabb5d0f067e78afd029d8ec41d14660d8f9e2
+Subproject commit 18c520e1c6d44a8c68328f3ffa4d64434fb454b8
index 3709434eab484a2df416a31b6ea7bfe311e7fba8..6be037eb9b858f3ef0dc1cdd34bf9296e377508e 100644 (file)
@@ -29,6 +29,17 @@ m4_traceoff([AM_GNU_GETTEXT])
 AM_GNU_GETTEXT([external])
 AH_BOTTOM([#include <conf_post.h>])
 
+# 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.])
index 97c21ad49b7e51b00e593d3c900c3c1f7f2b12ee..977700cdf761b9bc19db63dc42070b58e2b1ed66 100644 (file)
@@ -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 (file)
index 0000000..bf97caf
--- /dev/null
@@ -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 <https://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+#include <stdlib.h>
+#include <locale.h>
+#include <assert.h>
+#include <getopt.h>
+#include <curses.h>
+
+#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 (file)
index 0000000..fe0f942
--- /dev/null
@@ -0,0 +1,5 @@
+--version
+Print a version message and then exit.
+
+--help
+Print this message and then exit.