]> git.draconx.ca Git - rrace.git/commitdiff
Implement some basic gameplay.
authorNick Bowler <nbowler@draconx.ca>
Fri, 4 Mar 2022 07:17:05 +0000 (02:17 -0500)
committerNick Bowler <nbowler@draconx.ca>
Fri, 4 Mar 2022 07:17:05 +0000 (02:17 -0500)
12 files changed:
.gitignore
Makefile.am
configure.ac
src/game.c [new file with mode: 0644]
src/game.h [new file with mode: 0644]
src/motif.c
src/motif.h
src/motif_ui.c
src/x11.c [new file with mode: 0644]
t/boardmove.c [new file with mode: 0644]
tests/game.at [new file with mode: 0644]
testsuite.at [new file with mode: 0644]

index fa3499881db3f69b7381ec3b825eaee642d5c40c..2f2a3da8d12aa07fd0e6e17ba3d8ac475a93555e 100644 (file)
@@ -5,6 +5,7 @@
 /Makefile
 /Makefile.in
 /aclocal.m4
+/atconfig
 /autom4te.cache
 /compile
 /config.*
@@ -14,5 +15,9 @@
 /libtool
 /ltmain.sh
 /missing
+/package.m4
 /rrace-motif
 /stamp-h1
+/testsuite
+/testsuite.deps
+/testsuite.dir
index 96e734021c019fc61b8289b423e33cb2cfbf99ed..b0a7b755e170299c2f88e998fee955633b2bcd06 100644 (file)
@@ -12,12 +12,12 @@ MAINTAINERCLEANFILES =
 DISTCLEANFILES =
 CLEANFILES = $(EXTRA_LIBRARIES)
 
-AM_CPPFLAGS = -I$(DX_BASEDIR)/src
+AM_CPPFLAGS = -I$(builddir)/src -I$(srcdir)/src -I$(DX_BASEDIR)/src
 AM_CFLAGS = $(MOTIF_CFLAGS)
 
 bin_PROGRAMS = rrace-motif
 
-rrace_motif_SOURCES = #src/motif.c common/src/help.c
+rrace_motif_SOURCES = src/game.c src/x11.c
 rrace_motif_LDADD = $(libmotifmain_a_OBJECTS) $(libmotifui_a_OBJECTS) \
                     $(libglohelp_a_OBJECTS) $(MOTIF_LIBS)
 
@@ -49,3 +49,9 @@ GUIFILES = src/motifgui.dat
 $(GUIFILES:.dat=.h): $(DX_BASEDIR)/scripts/gen-tree.awk
 DISTCLEANFILES += $(GUIFILES:.dat=.h)
 EXTRA_DIST += $(DX_BASEDIR)/scripts/gen-tree.awk $(GUIFILES)
+
+check_PROGRAMS = t/boardmove t/rng-test
+
+t_boardmove_LDADD = src/game.$(OBJEXT)
+
+include $(top_srcdir)/common/snippet/autotest.mk
index fb7dd4101ae671c2bbdfb955a178b2a0361e5658..b0c7fb06355cb0916ee5d021694cbc7c0cc36f24 100644 (file)
@@ -35,5 +35,9 @@ AC_SUBST([MOTIF_LIBS], [@&t@])
 AS_IF([test x"$dx_cv_have_motif" = x"yes"],
   [MOTIF_CFLAGS=$dx_cv_motif_cflags MOTIF_LIBS=$dx_cv_motif_libs])
 
+AC_CONFIG_TESTDIR([.], [t:.])
+DX_PROG_AUTOTEST
+AM_CONDITIONAL([HAVE_AUTOTEST], [test x"$dx_cv_autotest_works" = x"yes"])
+
 AC_CONFIG_FILES([Makefile])
 AC_OUTPUT
diff --git a/src/game.c b/src/game.c
new file mode 100644 (file)
index 0000000..bd1fef9
--- /dev/null
@@ -0,0 +1,181 @@
+/*
+ * The RNG implementation is adapted from xoshiro256** and splitmix64
+ * by David Blackman and Sebastiano Vigna, originally distributed under
+ * the Creative Commons Zero public domain dedication.
+ */
+#include <config.h>
+#include <limits.h>
+#include <string.h>
+#include <time.h>
+#include "game.h"
+
+#define B64(x) ((x) & 0xffffffffffffffff)
+
+/* Rotate val left by n bits.  The behaviour is undefined if n is zero. */
+static unsigned long long rot_left64(unsigned long long val, int n)
+{
+       return B64( (val << n) | (val >> (64 - n)) );
+}
+
+static unsigned long long xoshiro256ss(unsigned long long *s)
+{
+       unsigned long long tmp, ret;
+
+       ret = B64(rot_left64(B64(s[1]*5), 7) * 9);
+       tmp = B64(s[1] << 17);
+
+       s[2] ^= s[0];
+       s[3] ^= s[1];
+       s[1] ^= s[2];
+       s[0] ^= s[3];
+       s[2] ^= tmp;
+       s[3] = rot_left64(s[3], 45);
+
+       return ret;
+}
+
+static unsigned long long splitmix64(unsigned long long *state)
+{
+       unsigned long long z;
+
+       z = B64(*state += 0x9e3779b97f4a7c15);
+       z = B64((z ^ (z >> 30)) * 0xbf58476d1ce4e5b9);
+       z = B64((z ^ (z >> 27)) * 0x94d049bb133111eb);
+
+       return z ^ (z >> 31);
+}
+
+static unsigned long long rng_state[4];
+
+static int rng_is_seeded(void)
+{
+       return rng_state[0] || rng_state[1] || rng_state[2] || rng_state[3];
+}
+
+/* Calculate the least power of two greater than val, minus 1. */
+static unsigned rng_mask(unsigned val)
+{
+       val |= val >> 1;
+       val |= val >> 2;
+       val |= val >> 4;
+       val |= val >> 8;
+
+       if (UINT_MAX >= 65536)
+               val |= val >> 16;
+
+       return val;
+}
+
+/* Return a random integer uniformly on the closed interval [0, limit-1] */
+static unsigned rng_uniform_int(unsigned max)
+{
+       unsigned mask = rng_mask(max-1);
+       unsigned long long val;
+
+       do {
+               val = ( xoshiro256ss(rng_state) >> 32 ) & mask;
+       } while (val >= max);
+
+       return val;
+}
+
+static void shuffle(unsigned char *tiles, unsigned n)
+{
+       unsigned i, j;
+
+       for (i = 1; i < n; i++) {
+               unsigned char tmp;
+
+               j = rng_uniform_int(i+1);
+               tmp = tiles[i];
+               tiles[i] = tiles[j];
+               tiles[j] = tmp;
+       }
+}
+
+void game_reseed(unsigned long long seed)
+{
+       rng_state[0] = splitmix64(&seed);
+       rng_state[1] = splitmix64(&seed);
+       rng_state[2] = splitmix64(&seed);
+       rng_state[3] = splitmix64(&seed);
+}
+
+void game_reset(struct board *board)
+{
+       unsigned char tiles[25];
+       unsigned i;
+
+       if (!rng_is_seeded())
+               game_reseed(time(NULL));
+
+       for (i = 0; i < 24; i++) {
+               tiles[i] = (i%6) + 1;
+       }
+
+       shuffle(tiles, 24);
+       memset(board->goal, 0, sizeof board->goal);
+
+       for (i = 0; i < 9; i++) {
+               uint_fast32_t position = board_position(i/3+1, i%3+1);
+
+               if (tiles[i] & 1)
+                       board->goal[0] |= position >> 6;
+               if (tiles[i] & 2)
+                       board->goal[1] |= position >> 6;
+               if (tiles[i] & 4)
+                       board->goal[2] |= position >> 6;
+       }
+
+       tiles[24] = TILE_EMPTY;
+       shuffle(tiles, 25);
+       memset(board->game, 0, sizeof board->game);
+
+       for (i = 0; i < 25; i++) {
+               unsigned x = i/5, y = i%5;
+               uint_fast32_t position;
+
+               position = board_position(x, y);
+               if (tiles[i] == TILE_EMPTY) {
+                       board->game[0] = 0x1ffffff ^ position;
+                       board->x = x;
+                       board->y = y;
+               } else {
+                       if (tiles[i] & 1)
+                               board->game[1] |= position;
+                       if (tiles[i] & 2)
+                               board->game[2] |= position;
+                       if (tiles[i] & 4)
+                               board->game[3] |= position;
+               }
+       }
+}
+
+int game_do_move(struct board *board, int x, int y)
+{
+       int bx = board->x, by = board->y;
+       uint_least32_t mask, val[4];
+       int i, shl, shr;
+
+       if ((bx != x) == (by != y))
+               return -1;
+
+       if (bx == x) {
+               mask = board_mask_v(x, by, y);
+               shr = 5*(by < y);
+               shl = 5*(by > y);
+       } else {
+               mask = board_mask_h(y, bx, x);
+               shr = bx < x;
+               shl = bx > x;
+       }
+
+       for (i = 0; i < 4; i++) {
+               board->game[i] ^= (val[i] = board->game[i] & mask);
+               board->game[i] |= val[i] << shl >> shr;
+       }
+
+       board->x = x;
+       board->y = y;
+       return 0;
+}
diff --git a/src/game.h b/src/game.h
new file mode 100644 (file)
index 0000000..f4473a7
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * Slide puzzle core game logic
+ * 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_GAME_H_
+#define RRACE_GAME_H_
+
+#include <inttypes.h>
+
+enum {
+       TILE_EMPTY,
+       TILE_RED,
+       TILE_ORANGE,
+       TILE_YELLOW,
+       TILE_GREEN,
+       TILE_BLUE,
+       TILE_WHITE,
+       TILE_MAX
+};
+
+enum { GOAL_SHIFT = 6 };
+struct board {
+       /*
+        * Bit planes representing the current game area.
+        *
+        * The 5x5 game board is represented by these four 25-bit values.
+        * The bits are arranged in row-major order so, for example, bit 0
+        * corresponds to position (0,0), bit 4 is position (4,0) and bit 25
+        * is position (4, 4).
+        *
+        *   game[0] - the board mask, all bits set except one indicating the
+        *             absense of a tile at this position.
+        *   game[1] - least significant bit of the tile's colour.
+        *   game[2] - tile colour.
+        *   game[3] - most significant bit of the tile's colour.
+        */
+       uint_least32_t game[4];
+
+       /*
+        * Bit planes representing the goal area.
+        *
+        * These are encoded identically to the game area, except the values
+        * are shifted right by 6 as only bits 6 through 18 are relevant.
+        *
+        *   goal[0] - least significant bit of the tile's colour.
+        *   goal[2] - tile colour.
+        *   goal[3] - most significant bit of the tile's colour.
+        */
+       uint_least16_t goal[3];
+
+       /* (x, y) position of the current empty position. */
+       uint_least8_t x, y;
+};
+
+/* Return the board bitmap with all bits in column x set */
+static inline uint_fast32_t board_column(int x)
+{
+       return 0x108421ul << x;
+}
+
+/* Return the board bitmap with all bits in row y set */
+static inline uint_fast32_t board_row(int y)
+{
+       return 0x1ful << 5*y;
+}
+
+/* Return the board bitmap with the bit at position (x, y) set. */
+static inline uint_fast32_t board_position(int x, int y)
+{
+       return 1ul << x << 5*y;
+}
+
+/*
+ * Return the board bitmap with set bits indicating tile locations
+ * that change with the hole at (x0, y) and the play at (x1, y).
+ */
+static inline uint_fast32_t board_mask_h(int y, int x0, int x1)
+{
+       uint_fast32_t row = board_row(y);
+
+       if (x0 < x1)
+               return (row << x0) & (row >> (4-x1));
+       return (row << x1) & (row >> (4-x0));
+}
+
+/*
+ * Return the board bitmap with set bits indicating tile locations
+ * that change with the hole at (x, y0) and the play at (x, y1).
+ */
+static inline uint_fast32_t board_mask_v(int x, int y0, int y1)
+{
+       uint_fast32_t col = board_column(x);
+
+       if (y0 < y1)
+               return (col << 5*y0) & (col >> 5*(4-y1));
+       return (col << 5*y1) & (col >> 5*(4-y0));
+}
+
+/*
+ * Move the bits in the game bitmaps according to a move at position (x, y),
+ * and update the location of the empty position which, if the move was valid
+ * is now (x, y).
+ *
+ * Returns 0 if the move was valid (and board has been updated), -1 otherwise.
+ */
+int game_do_move(struct board *board, int x, int y);
+
+/*
+ * Initialize the game RNG such that the next call to game_reset will produce a
+ * new board that is entirely dependent on the given seed value.
+ */
+void game_reseed(unsigned long long seed);
+
+/*
+ * Shuffle the game and goal tiles to produce a new game state.
+ */
+void game_reset(struct board *board);
+
+#endif
index cce9f3fac944c5e4910a83312933490d2264a7dd..96d8e086666633428bb0d9c54aba5629c9b6f185 100644 (file)
@@ -26,6 +26,7 @@
 #include "help.h"
 #include "motif.h"
 #include "options.h"
+#include "game.h"
 
 #define PROGNAME "rrace"
 static const char *progname = PROGNAME;
@@ -38,6 +39,7 @@ static char * const default_resources[] = {
        PROGNAME "*game.XmFrame.shadowThickness: 3",
        PROGNAME "*game.XmFrame.shadowType: shadow_in",
        PROGNAME "*goalArea.leftOffset: 1",
+       PROGNAME "*gameCanvas.background: #404040",
 
        NULL
 };
@@ -122,6 +124,7 @@ static Widget early_setup(XtAppContext *app, int argc, char **argv)
 
 static XtAppContext app_initialize(int argc, char **argv)
 {
+       static struct app_state state;
        XtAppContext app;
        Widget shell;
 
@@ -129,7 +132,9 @@ static XtAppContext app_initialize(int argc, char **argv)
                progname = argv[0];
 
        shell = early_setup(&app, argc, argv);
-       ui_initialize(shell);
+       ui_initialize(&state, shell);
+       x11_initialize(&state, XtScreen(shell));
+       game_reset(&state.board);
        XtRealizeWidget(shell);
 
        return app;
index f3376188dce306d65b66495d5de6b1ad591e0026..ae6631d173b27b87d1590b60acf4b447878f597b 100644 (file)
 #ifndef RRACE_MOTIF_H_
 #define RRACE_MOTIF_H_
 
+#include <inttypes.h>
 #include <X11/Intrinsic.h>
+#include "game.h"
 
-void ui_initialize(Widget shell);
+enum { COLOUR_PRIMARY, COLOUR_DARK, COLOUR_LIGHT, COLOUR_MAX };
+
+struct app_state {
+       struct board board;
+
+       Widget game, goal;
+
+       GC tile_gc;
+       uint_least32_t tile_colour[TILE_MAX-1][3];
+};
+
+void ui_initialize(struct app_state *state, Widget shell);
+void x11_initialize(struct app_state *state, Screen *screen);
+void x11_redraw_goal(struct app_state *state);
+void x11_redraw_game(struct app_state *state);
 
 #endif
index 0708b4029ba41152c6d3740f9ab3028b5707b904..fbef33b73334fad576410ed2911f3fff56871948 100644 (file)
@@ -123,31 +123,52 @@ construct_widgets(const struct ui_widget *root, Widget parent, unsigned i)
 
 static void game_resize(Widget w, void *data, void *cb_data)
 {
-       Dimension width, height;
-
-       XtVaGetValues(w, XmNwidth, &width, XmNheight, &height, (char *)NULL);
-
-       printf("game %ux%u\n", width, height);
+       if (XtIsRealized(w))
+               x11_redraw_game(data);
 }
 
 static void goal_resize(Widget w, void *data, void *cb_data)
 {
+       if (XtIsRealized(w))
+               x11_redraw_goal(data);
+}
+
+static void game_input(Widget w, void *data, void *cb_data)
+{
+       XmDrawingAreaCallbackStruct *cbs = cb_data;
+       XButtonEvent *click = &cbs->event->xbutton;
+       struct app_state *state = data;
        Dimension width, height;
+       unsigned x = -1, y = -1;
+
+       switch (cbs->event->type) {
+       case ButtonPress:
+               XtVaGetValues(w, XmNwidth, &width,
+                                XmNheight, &height,
+                                (char *)NULL);
+               x = click->x / (width/5);
+               y = click->y / (width/5);
+       }
 
-       XtVaGetValues(w, XmNwidth, &width, XmNheight, &height, (char *)NULL);
+       if (x > 4 || y > 4)
+               return;
 
-       printf("goal %ux%u\n", width, height);
+       if (game_do_move(&state->board, x, y) == 0) {
+               x11_redraw_game(state);
+       }
 }
 
-void ui_initialize(Widget shell)
+void ui_initialize(struct app_state *state, Widget shell)
 {
-       Widget game, goal;
-
        construct_widgets(mainwin, shell, 0);
 
-       game = XtNameToWidget(shell, "*gameCanvas");
-       goal = XtNameToWidget(shell, "*goalCanvas");
+       state->game = XtNameToWidget(shell, "*gameCanvas");
+       state->goal = XtNameToWidget(shell, "*goalCanvas");
+
+       XtAddCallback(state->game, XmNresizeCallback, game_resize, state);
+       XtAddCallback(state->game, XmNexposeCallback, game_resize, state);
+       XtAddCallback(state->game, XmNinputCallback,  game_input,  state);
 
-       XtAddCallback(game, XmNresizeCallback, game_resize, NULL);
-       XtAddCallback(goal, XmNresizeCallback, goal_resize, NULL);
+       XtAddCallback(state->goal, XmNresizeCallback, goal_resize, state);
+       XtAddCallback(state->goal, XmNexposeCallback, goal_resize, state);
 }
diff --git a/src/x11.c b/src/x11.c
new file mode 100644 (file)
index 0000000..3b12bd3
--- /dev/null
+++ b/src/x11.c
@@ -0,0 +1,152 @@
+/*
+ * X11 GUI 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 <stdio.h>
+#include <assert.h>
+#include <Xm/XmStrDefs.h>
+#include "motif.h"
+
+/* TODO user-selectable colours */
+static const char * const colours[][3] = {
+       /*primary    bottom      top */
+       "#fa2e2e", "#c02323", "#ff3939", /* red */
+       "#ff8c00", "#c46c00", "#ffa400", /* orange */
+       "#ffd700", "#c4a500", "#ffed00", /* yellow */
+       "#228b22", "#186318", "#27a027", /* green */
+       "#4682b4", "#325c80", "#4c8dc3", /* blue */
+       "#faf0e6", "#b2aaa3", "#fff9ee", /* white */
+};
+
+static void init_colours(struct app_state *state, Screen *screen)
+{
+       Display *display = DisplayOfScreen(screen);
+       Colormap cmap = DefaultColormapOfScreen(screen);
+       XColor colour, junk;
+       unsigned i, j;
+
+       for (j = 0; j < COLOUR_MAX; j++) {
+               for (i = 0; i < TILE_MAX-1; i++) {
+                       XAllocNamedColor(display, cmap, colours[i][j],
+                                                 &colour, &junk);
+                       state->tile_colour[i][j] = colour.pixel;
+               }
+       }
+}
+
+void x11_initialize(struct app_state *state, Screen *screen)
+{
+       Display *display = DisplayOfScreen(screen);
+       Window root = RootWindowOfScreen(screen);
+       Colormap cmap = DefaultColormapOfScreen(screen);
+       XGCValues gcv;
+
+       init_colours(state, screen);
+
+       gcv.line_width = 1;
+       state->tile_gc = XCreateGC(display, root, GCLineWidth, &gcv);
+}
+
+static void draw_tile(struct app_state *state, Display *display, Drawable d,
+                      int tile, int gx, int gy, Dimension tw, Dimension th)
+{
+       int tx = gx * tw, ty = gy * th;
+
+       XSegment topshadow[] = {
+               { tx,      ty,      tx,      ty+th   },
+               { tx+1,    ty,      tx+tw,   ty      },
+
+               { tx+1,    ty+1,    tx+1,    ty+th-1 },
+               { tx+2,    ty+1,    tx+tw-1, ty+1    }
+       };
+
+       XSegment bottomshadow[] = {
+               { tx+1,    ty+th-1, tx+tw,   ty+th-1 },
+               { tx+tw-1, ty+th-1, tx+tw-1, ty+1    },
+
+               { tx+2,    ty+th-2, tx+tw-1, ty+th-2 },
+               { tx+tw-2, ty+th-2, tx+tw-2, ty+2    }
+       };
+
+       XSetForeground(display, state->tile_gc, state->tile_colour[tile-1][COLOUR_LIGHT]);
+       XDrawSegments(display, d, state->tile_gc, topshadow, XtNumber(topshadow));
+
+       XSetForeground(display, state->tile_gc, state->tile_colour[tile-1][COLOUR_DARK]);
+       XDrawSegments(display, d, state->tile_gc, bottomshadow, XtNumber(bottomshadow));
+
+       XSetForeground(display, state->tile_gc, state->tile_colour[tile-1][COLOUR_PRIMARY]);
+       XFillRectangle(display, d, state->tile_gc, tx+2, ty+2, tw-4, th-4);
+}
+
+static void
+redraw_tile(struct app_state *state, Display *display, Drawable d,
+            uint_fast32_t bit0, uint_fast32_t bit1, uint_fast32_t bit2,
+            int x, int y, Dimension w, Dimension h)
+{
+       uint_fast32_t pos = board_position(x, y);
+       unsigned char tile = 0;
+
+       if (bit0 & pos) tile |= 1;
+       if (bit1 & pos) tile |= 2;
+       if (bit2 & pos) tile |= 4;
+       assert(tile < TILE_MAX);
+
+       if (tile == TILE_EMPTY) {
+               XClearArea(display, d, x*w, y*h, w, h, 0);
+       } else {
+               draw_tile(state, display, d, tile, x, y, w, h);
+       }
+}
+
+void x11_redraw_goal(struct app_state *state)
+{
+       Display *display = XtDisplay(state->goal);
+       Window goal = XtWindow(state->goal);
+       Dimension w, h;
+       int i;
+
+       XtVaGetValues(state->goal, XmNwidth, &w, XmNheight, &h, (char *)NULL);
+       w /= 3; h /= 3;
+
+       for (i = 0; i < 9; i++) {
+               uint_least16_t *gp = state->board.goal;
+
+               redraw_tile(state, display, goal,
+                           gp[0], gp[1], gp[2],
+                           i%3, i/3, w, h);
+       }
+}
+
+void x11_redraw_game(struct app_state *state)
+{
+       Display *display = XtDisplay(state->goal);
+       Window game = XtWindow(state->game);
+       Dimension w, h;
+       int i;
+
+       XtVaGetValues(state->game, XmNwidth, &w, XmNheight, &h, (char *)NULL);
+       w /= 5; h /= 5;
+
+       for (i = 0; i < 25; i++) {
+               uint_least32_t *gp = state->board.game+1;
+
+               redraw_tile(state, display, game,
+                           gp[0], gp[1], gp[2],
+                           i%5, i/5, w, h);
+       }
+}
diff --git a/t/boardmove.c b/t/boardmove.c
new file mode 100644 (file)
index 0000000..89b3460
--- /dev/null
@@ -0,0 +1,87 @@
+#include <config.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "game.h"
+
+static const char *progname;
+
+void show_board(uint_fast32_t shape)
+{
+       unsigned i, j;
+
+       for (i = 0; i < 5; i++) {
+               unsigned row = (shape >> 5*i) & 0x1f;
+
+               for (j = 0; j < 5; j++) {
+                       printf("%c", row & 1 ? '@' : '.');
+                       row >>= 1;
+               }
+               putchar('\n');
+       }
+}
+
+static void print_usage(FILE *f)
+{
+       fprintf(f, "Usage: %s sequence\n", progname);
+}
+
+static int get_seq(char c)
+{
+       if (c >= '0' && c <= '4') {
+               return c - '0';
+       }
+
+       if (c == 0) {
+               fprintf(stderr, "%s: unexpected end of sequence\n", progname);
+       } else {
+               fprintf(stderr, "%s: invalid character %c\n", progname, c);
+       }
+       exit(EXIT_FAILURE);
+}
+
+int main(int argc, char **argv)
+{
+       struct board board;
+       const char *seq;
+       int i = 0;
+
+       if (argv > 0)
+               progname = argv[0];
+
+       if (argc != 2) {
+               print_usage(stderr);
+               return EXIT_FAILURE;
+       }
+
+       seq = argv[1];
+       board.x = get_seq(seq[i++]);
+       board.y = get_seq(seq[i++]);
+
+       board.game[0] = 0x1ffffff ^ board_position(board.x, board.y);
+       board.game[1] = board.game[2] = board.game[3] = board.game[0];
+
+       while (1) {
+               int j, x, y;
+
+               show_board(board.game[0]);
+               for (j = 1; j < 4; j++) {
+                       if (board.game[j] != board.game[0]) {
+                               fprintf(stderr, "%s: plane %d mismatch\n",
+                                               progname, j);
+                               board.game[j] = board.game[0];
+                       }
+               }
+
+               if (seq[i]) {
+                       x = get_seq(seq[i++]);
+                       y = get_seq(seq[i++]);
+                       game_do_move(&board, x, y);
+                       putchar('\n');
+               } else {
+                       break;
+               }
+       }
+
+       return 0;
+}
diff --git a/tests/game.at b/tests/game.at
new file mode 100644 (file)
index 0000000..c90ff87
--- /dev/null
@@ -0,0 +1,542 @@
+AT_SETUP([game_do_move zigzag])
+
+AT_CHECK([boardmove m4_do(
+  [0010203040],
+  [4131211101],
+  [0212223242],
+  [4333231303],
+  [0414243444])], [0],
+[[.@@@@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@.@@@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@.@@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@.@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@.
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@.
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@.@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@.@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@.@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+.@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+.@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@.@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@.@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@.@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@.
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@.
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@.@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@.@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@.@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+.@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+.@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@.@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@@.@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@@@.@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@@@@.
+]])
+
+AT_CLEANUP
+
+AT_SETUP([game_do_move vertical])
+
+AT_CHECK([boardmove m4_do(
+  [020103000402],
+  [121311141012],
+  [222123202422],
+  [323331343032],
+  [424143404442])], [0],
+[[@@@@@
+@@@@@
+.@@@@
+@@@@@
+@@@@@
+
+@@@@@
+.@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+.@@@@
+@@@@@
+
+.@@@@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+.@@@@
+
+@@@@@
+@@@@@
+.@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@.@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@.@@@
+@@@@@
+
+@@@@@
+@.@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@.@@@
+
+@.@@@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@.@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@.@@
+@@@@@
+@@@@@
+
+@@@@@
+@@.@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@.@@
+@@@@@
+
+@@.@@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@@.@@
+
+@@@@@
+@@@@@
+@@.@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@.@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@.@
+@@@@@
+
+@@@@@
+@@@.@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@@@.@
+
+@@@.@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@.@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@.
+@@@@@
+@@@@@
+
+@@@@@
+@@@@.
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@.
+@@@@@
+
+@@@@.
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@@@@.
+
+@@@@@
+@@@@@
+@@@@.
+@@@@@
+@@@@@
+]])
+
+AT_CLEANUP
+
+AT_SETUP([game_do_move horizontal])
+
+AT_CHECK([boardmove m4_do(
+  [203010400020],
+  [211131014121],
+  [223212420222],
+  [231333034323],
+  [243414440424])], [0],
+[[@@.@@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@.@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@.@@@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@.
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+.@@@@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@.@@
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@.@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@.@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@.@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+.@@@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@.
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@.@@
+@@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@.@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@.@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@.@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@.
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+.@@@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@.@@
+@@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@.@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@.@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@.@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+.@@@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@.
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@.@@
+@@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@@.@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@@@.@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@.@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@@@@.
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+.@@@@
+
+@@@@@
+@@@@@
+@@@@@
+@@@@@
+@@.@@
+]])
+
+AT_CLEANUP
diff --git a/testsuite.at b/testsuite.at
new file mode 100644 (file)
index 0000000..7e8855e
--- /dev/null
@@ -0,0 +1,19 @@
+AT_COPYRIGHT([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/>.
+
+AT_INIT
+AT_COLOR_TESTS
+
+m4_include([tests/game.at])