/Makefile
/Makefile.in
/aclocal.m4
+/atconfig
/autom4te.cache
/compile
/config.*
/libtool
/ltmain.sh
/missing
+/package.m4
/rrace-motif
/stamp-h1
+/testsuite
+/testsuite.deps
+/testsuite.dir
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)
$(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
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
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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
#include "help.h"
#include "motif.h"
#include "options.h"
+#include "game.h"
#define PROGNAME "rrace"
static const char *progname = PROGNAME;
PROGNAME "*game.XmFrame.shadowThickness: 3",
PROGNAME "*game.XmFrame.shadowType: shadow_in",
PROGNAME "*goalArea.leftOffset: 1",
+ PROGNAME "*gameCanvas.background: #404040",
NULL
};
static XtAppContext app_initialize(int argc, char **argv)
{
+ static struct app_state state;
XtAppContext app;
Widget shell;
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;
#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
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);
}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+#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;
+}
--- /dev/null
+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
--- /dev/null
+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])