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)
$(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 $@
--- /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 <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;
+ }
+ }
+}