#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
+enum {
+ GAME_YPOS = 1 // top row of game and goal areas.
+};
+
static const char *progname = "rrace";
static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
struct board board;
WINDOW *gamewin[WINDOW_MAX], *goalwin[WINDOW_MAX];
+ WINDOW *timer;
+
+ /* Most recently displayed timer value, for screen redraw. */
+ uint_least32_t timer_ms;
+
+ /* Location of the keyboard cursor */
+ int_least8_t cursor;
+
+ /* If true, the goal will be displayed over the main play area. */
+ uint_least8_t view_goal_on_game;
+
+ /* Clicked toolbar item */
+ uint_least8_t toolbar_click;
} state;
static void print_version(void)
printf("Report bugs to <%s>.\n", PACKAGE_BUGREPORT);
}
-static void draw_tile(WINDOW **win, unsigned colour,
+static void draw_tile(WINDOW **win, unsigned colour, unsigned selected,
unsigned x, unsigned y, unsigned start_column)
{
WINDOW *border = win[WINDOW_TILEBORDER], *fill = win[WINDOW_TILEFILL];
- int attr, ch;
- int w, h;
+ int w, h, attr, ch, bc = selected ? '#' : 0;
assert(colour < TILE_MAX);
attr = COLOR_PAIR(colour);
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;
+
+ case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(TILE_MAX);
}
getmaxyx(border, h, w);
w = 2*(w+1)/2;
- if (mvwin(border, 2+h*y, start_column+w*x) == ERR)
+ if (mvwin(border, 1+GAME_YPOS+h*y, start_column+w*x) == ERR)
return;
if (colour != TILE_EMPTY) {
- wattrset(border, attr);
- box(border, 0, 0);
-
mvderwin(fill, 1, 1);
wbkgdset(fill, A_REVERSE|attr|ch);
werase(fill);
werase(border);
}
+ if (bc || colour) {
+ wattrset(border, attr);
+ wborder(border, bc, bc, bc, bc, bc, bc, bc, bc);
+ }
+
wnoutrefresh(border);
}
static int
redraw_tile(WINDOW **win, unsigned x, unsigned y, unsigned start_column,
- uint_fast32_t bit0, uint_fast32_t bit1, uint_fast32_t bit2)
+ uint_fast32_t bit0, uint_fast32_t bit1, uint_fast32_t bit2,
+ unsigned selected)
{
uint_fast32_t pos = board_position(x, y);
unsigned char tile = 0;
if (bit2 & pos) tile |= 4;
assert(tile < TILE_MAX);
- draw_tile(win, tile, x, y, start_column);
+ draw_tile(win, tile, selected, x, y, start_column);
return tile;
}
getmaxyx(stdscr, h, w);
- if (h <= 2) {
+ if (h <= GAME_YPOS+1) {
bl = ACS_ULCORNER, br = ACS_URCORNER;
- } else if (h <= 3*sz+2) {
+ } else if (h <= 3*sz+GAME_YPOS+1) {
bl = br = ACS_VLINE, bs = ' ';
}
static void curs_redraw_game(struct app_state *state, uint_fast32_t mask)
{
- uint_least32_t *gp = state->board.game;
+ uint_least32_t buf[3], *gp = state->board.game;
int i;
if (mask == -1)
redraw_area_border(state->gamewin, 2, 5);
+ if (state->view_goal_on_game) {
+ for (i = 0; i < 3; i++) {
+ buf[i] = state->board.goal[i];
+ buf[i] = (gp[i] & ~GOAL_MASK) | (buf[i] << GOAL_SHIFT);
+ }
+ gp = buf;
+ }
+
for (i = 0; i < 25; i++) {
if (mask & 1) {
redraw_tile(state->gamewin, i%5, i/5,
- 4, gp[0], gp[1], gp[2]);
+ 4, gp[0], gp[1], gp[2],
+ i == state->cursor);
}
mask >>= 1;
}
for (i = 0; i < 9; i++) {
if (mask & 1) {
redraw_tile(state->goalwin, i%3, i/3,
- x+2, gp[0], gp[1], gp[2]);
+ x+2, gp[0], gp[1], gp[2], 0);
}
mask >>= 1;
}
/* Frame for game area */
w = MIN(scr_w-2, 3+10*gamesz);
- h = MIN(scr_h-1, 2+5*gamesz);
- realloc_area(&state->gamewin[WINDOW_AREA], h, w, 1, 2);
+ h = MIN(scr_h-GAME_YPOS, 2+5*gamesz);
+ realloc_area(&state->gamewin[WINDOW_AREA], h, w, GAME_YPOS, 2);
/* Frame for goal area */
w = MIN(scr_w-split, 3+6*goalsz);
- h = MIN(scr_h-1, 2+3*goalsz);
- realloc_area(&state->goalwin[WINDOW_AREA], h, w, 1, split);
+ h = MIN(scr_h-GAME_YPOS, 2+3*goalsz);
+ realloc_area(&state->goalwin[WINDOW_AREA], h, w, GAME_YPOS, split);
+
+ /* Status area */
+ w = MAX(0, scr_w-split-1);
+ realloc_area(&state->timer, 1, w, GAME_YPOS+h, split+1);
}
static void app_initialize(int argc, char **argv)
}
game_reset(&state.board);
+ state.cursor = -1;
initscr();
start_color();
- if (curs_set(0) != ERR)
- leaveok(stdscr, TRUE);
+ curs_set(0);
cbreak();
keypad(stdscr, TRUE);
if (enable_mouse) {
+ /*
+ * While we only care about a subset of these events, for
+ * some reason with ncurses failing to enable all of them
+ * causes timeout to stop working when disabled buttons are
+ * pressed (or released).
+ *
+ * It is not known if other curses have this problem; at least
+ * pdcurses does not, but the extra events should be harmless
+ * in any case.
+ */
+#if HAVE_CURSES_MOUSE_SUPPORT
+ unsigned long mask = BUTTON1_PRESSED | BUTTON1_RELEASED
+ | BUTTON2_PRESSED | BUTTON2_RELEASED
+ | BUTTON3_PRESSED | BUTTON3_RELEASED
+#ifdef BUTTON4_PRESSED
+ | BUTTON4_PRESSED | BUTTON4_RELEASED
+#endif
+#ifdef BUTTON5_PRESSED
+ | BUTTON5_PRESSED | BUTTON5_RELEASED
+#endif
+ ;
#if HAVE_CURSES_MOUSE_SET
- mouse_set(BUTTON1_PRESSED);
+ mouse_set(mask);
#elif HAVE_CURSES_MOUSEMASK
- mousemask(BUTTON1_PRESSED, NULL);
+ mousemask(mask, NULL);
#endif
#if HAVE_CURSES_MOUSEINTERVAL
mouseinterval(0);
#endif
+#endif /* HAVE_CURSES_MOUSE_SUPPORT */
}
noecho();
init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
+ init_pair(TILE_MAX, COLOR_BLACK, COLOR_BLACK);
setup_mainwin(&state);
refresh();
}
-static void do_move(struct app_state *state, int x, int y)
+static void update_timer(struct app_state *state, uint_fast32_t ms)
+{
+ unsigned sec, min;
+
+ state->timer_ms = ms;
+ sec = ms / 1000; ms %= 1000;
+ min = sec / 60; sec %= 60;
+ mvwprintw(state->timer, 0, 0, "Time: %u:%.2u.%.3u",
+ min, sec, (unsigned)ms);
+ wclrtoeol(state->timer);
+ wrefresh(state->timer);
+}
+
+static uint_fast32_t do_move(struct app_state *state, int x, int y)
{
uint_fast32_t mask;
if ((mask = game_do_move(&state->board, x, y)) != 0) {
+ uint_fast32_t goal = game_check_goal(&state->board);
+
+ if (state->view_goal_on_game) {
+ state->view_goal_on_game = 0;
+ mask |= goal;
+ }
+
+ if (goal == 0) {
+ /* Solved! */
+ update_timer(state, game_finish(&state->board));
+ mask |= ~GOAL_MASK;
+ state->cursor = -1;
+ timeout(-1);
+ }
+
curs_redraw_game(state, mask);
- refresh();
+ doupdate();
}
+
+ return mask;
+}
+
+static void do_reset_cursor(struct app_state *state)
+{
+ state->cursor = 5*state->board.y + state->board.x;
+}
+
+static void do_new_game(struct app_state *state)
+{
+ game_reset(&state->board);
+
+ do_reset_cursor(state);
+ curs_redraw_game(state, -1);
+ curs_redraw_goal(state, -1);
+ update_timer(state, 0);
+ doupdate();
+
+ timeout(33);
+ game_begin(&state->board);
}
#if HAVE_CURSES_MOUSE_SUPPORT
y = MOUSE_Y_POS;
bstate = 0;
- if (BUTTON_CHANGED(1)) {
- switch (BUTTON_STATUS(1)) {
- case BUTTON_RELEASED: bstate |= BUTTON1_RELEASED;
- case BUTTON_PRESSED: bstate |= BUTTON1_PRESSED;
- }
- }
+
+#define set_bstate_helper(button) do { if (BUTTON_CHANGED(button)) \
+ switch (BUTTON_STATUS(button)) { \
+ case BUTTON_RELEASED: bstate |= BUTTON ## button ## _RELEASED; break; \
+ case BUTTON_PRESSED: bstate |= BUTTON ## button ## _PRESSED; break; \
+ } \
+} while (0);
+
+ set_bstate_helper(1);
+ set_bstate_helper(3);
#endif
- if (bstate == BUTTON1_PRESSED) {
+ if (bstate & BUTTON3_PRESSED) {
+ state->view_goal_on_game = 1;
+ curs_redraw_game(state, game_check_goal(&state->board));
+ doupdate();
+ }
+
+ if (bstate & BUTTON3_RELEASED) {
+ state->view_goal_on_game = 0;
+ curs_redraw_game(state, game_check_goal(&state->board));
+ doupdate();
+ }
+
+ if (!state->view_goal_on_game && bstate & BUTTON1_PRESSED) {
+ uint_fast32_t cursor_mask, move_mask;
int w, h;
/* Determine size of the game area */
w = 2*(w+1)/2;
if (x < 4 || (x -= 4)/5 >= w) return;
- if (y < 2 || (y -= 2)/5 >= h) return;
+ if (y <= GAME_YPOS || (y -= GAME_YPOS+1)/5 >= h) return;
+
+ /* Turn off the keyboard cursor when using the mouse */
+ cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor;
+ state->cursor = -1;
- do_move(state, x/w, y/h);
+ move_mask = do_move(state, x/w, y/h);
+ if ((cursor_mask & move_mask) == 0) {
+ curs_redraw_game(state, cursor_mask);
+ doupdate();
+ }
}
}
#endif
+static void do_move_cursor(struct app_state *state, int c)
+{
+ uint_fast32_t mask = 0;
+
+ if (state->cursor < 0) {
+ /* Cursor was hidden; reset it */
+ do_reset_cursor(state);
+ } else {
+ mask = 1ul << state->cursor;
+ }
+
+ if (state->view_goal_on_game) {
+ state->view_goal_on_game = 0;
+ mask |= game_check_goal(&state->board);
+ }
+
+ switch (c) {
+ case KEY_UP:
+ if ((state->cursor -= 5) < 0)
+ state->cursor += 25;
+ break;
+ case KEY_DOWN:
+ if ((state->cursor += 5) >= 25)
+ state->cursor -= 25;
+ break;
+ case KEY_LEFT:
+ if ((state->cursor -= 1) % 5 == 4 || state->cursor < 0)
+ state->cursor += 5;
+ break;
+ case KEY_RIGHT:
+ if ((state->cursor += 1) % 5 == 0)
+ state->cursor -= 5;
+ break;
+ }
+
+ curs_redraw_game(state, mask | 1ul << state->cursor);
+ doupdate();
+}
+
+static void do_keystroke(struct app_state *state, int c)
+{
+ switch (c) {
+ case KEY_DOWN: case KEY_UP: case KEY_LEFT: case KEY_RIGHT:
+ do_move_cursor(state, c);
+ break;
+ case '\t':
+ state->view_goal_on_game ^= 2u;
+ curs_redraw_game(state, game_check_goal(&state->board));
+ doupdate();
+ break;
+ case ' ':
+ if (!(state->view_goal_on_game & 1) && state->cursor >= 0)
+ do_move(state, state->cursor%5, state->cursor/5);
+ break;
+ }
+}
+
int main(int argc, char **argv)
{
setlocale(LC_ALL, "");
app_initialize(argc, argv);
- curs_redraw_game(&state, -1);
- curs_redraw_goal(&state, -1);
- refresh();
+ do_new_game(&state);
while (1) {
int c = getch();
refresh();
curs_redraw_game(&state, -1);
curs_redraw_goal(&state, -1);
- refresh();
+ update_timer(&state, state.timer_ms);
break;
#endif
#if HAVE_CURSES_MOUSE_SUPPORT
do_mouse(&state);
break;
#endif
+ default:
+ do_keystroke(&state, c);
+ case ERR:;
}
+
+ if (state.board.x <= 4)
+ update_timer(&state, game_elapsed(&state.board));
}
}