From: Nick Bowler Date: Fri, 4 Mar 2022 07:17:05 +0000 (-0500) Subject: Implement some basic gameplay. X-Git-Url: http://git.draconx.ca/gitweb/rrace.git/commitdiff_plain/2529a9651d160ab3a17118d778f5e5584d040765 Implement some basic gameplay. --- diff --git a/.gitignore b/.gitignore index fa34998..2f2a3da 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Makefile.am b/Makefile.am index 96e7340..b0a7b75 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 diff --git a/configure.ac b/configure.ac index fb7dd41..b0c7fb0 100644 --- a/configure.ac +++ b/configure.ac @@ -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 index 0000000..bd1fef9 --- /dev/null +++ b/src/game.c @@ -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 +#include +#include +#include +#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 index 0000000..f4473a7 --- /dev/null +++ b/src/game.h @@ -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 . + */ + +#ifndef RRACE_GAME_H_ +#define RRACE_GAME_H_ + +#include + +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 diff --git a/src/motif.c b/src/motif.c index cce9f3f..96d8e08 100644 --- a/src/motif.c +++ b/src/motif.c @@ -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; diff --git a/src/motif.h b/src/motif.h index f337618..ae6631d 100644 --- a/src/motif.h +++ b/src/motif.h @@ -19,8 +19,24 @@ #ifndef RRACE_MOTIF_H_ #define RRACE_MOTIF_H_ +#include #include +#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 diff --git a/src/motif_ui.c b/src/motif_ui.c index 0708b40..fbef33b 100644 --- a/src/motif_ui.c +++ b/src/motif_ui.c @@ -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 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 . + */ + +#include +#include +#include +#include +#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 index 0000000..89b3460 --- /dev/null +++ b/t/boardmove.c @@ -0,0 +1,87 @@ +#include +#include +#include + +#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 index 0000000..c90ff87 --- /dev/null +++ b/tests/game.at @@ -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 index 0000000..7e8855e --- /dev/null +++ b/testsuite.at @@ -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 . + +AT_INIT +AT_COLOR_TESTS + +m4_include([tests/game.at])