--- /dev/null
+/*
+ * Copyright (c) 2023, Matthew Mondor
+ * ALL RIGHTS RESERVED.
+ *
+ * Interactive curses user input test 8, derived from tests 1 and 7.
+ *
+ * Explicit termios to save and restore the tty(4) state is dropped as
+ * tests being more careful to avoid lower terminfo functionality seemed
+ * to now properly restore the state on OSX. This may also make porting to
+ * non-unix systems easier.
+ *
+ * We return to the sleeping method then checking for any user input in
+ * non-blocking 0-delay mode. This is a simpler design and may be more easy
+ * to port to non-Unix systems.
+ * Some drawbacks are possible:
+ * - Input sequence interpretation may require minimal internal delays to
+ * function, like for arrow key detection. So far this test still works
+ * on both NetBSD+Curses and Linux+NCurses. At lower bandwidth it may not
+ * always work as well, but an explicit flush was also required because the
+ * keyboard rate was faster than the clock, meaning that a fair number of
+ * characters get buffered in the input queue, mitigating this potential
+ * problem. It may also be possible that a delay of 0 really still results
+ * in short internal delay in the tty(4) processing put in modes controlled
+ * by curses and/or other timers in its implementation.
+ * - Assuming that the bandwidth is lower and that a system cannot keep up
+ * with the game frame rate refresh, the delay will immediately cause jumps
+ * to halve or quarter the FPS, etc.
+ * - The actual game processing and refresh rate may not always take the same
+ * time but our delay is fixed, meaning that the actual FPS rate may
+ * slightly vary.
+ * - It would be possible to perform processing and refresh and then query
+ * the time, potentially allowing the delay to be shortened depending on
+ * processing time. This implemenation doesn't even attempt to.
+ *
+ * $ cc -Wall -O0 -g -o test8 test8.c -lcurses
+ */
+
+#include <curses.h>
+#include <err.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+
+#define TERM_COLUMNS 79
+#define TERM_LINES 24
+#define ANIM_FPS 15
+#define NOBJECTS 46
+
+
+/* Animation object */
+
+enum aobject_type {
+ AOT_AVATAR = 0,
+ AOT_STAR,
+ AOT_BUG
+};
+
+typedef struct aobject {
+ int t, x, y, col;
+ char c;
+} aobject_t;
+
+
+int main(void);
+static void cleanup(void);
+static void anim_init(void);
+static void anim_redraw(void);
+static void anim_update(void);
+
+
+/* Curses window */
+static WINDOW *w = NULL;
+
+/* Animation world */
+static aobject_t objects[NOBJECTS];
+static aobject_t avatar = { AOT_AVATAR,
+ TERM_COLUMNS / 2, TERM_LINES / 2,
+ 0, '?' };
+
+
+int
+main(void)
+{
+
+ /* Setup curses */
+ if ((w = initscr()) == NULL)
+ err(EXIT_FAILURE, "initscr()");
+ if (COLS < TERM_COLUMNS || LINES < TERM_LINES) {
+ cleanup();
+ (void)fprintf(stderr,
+ "The terminal must support at least %d columns by %d"
+ " lines for this program to work.\n",
+ TERM_COLUMNS, TERM_LINES);
+ exit(EXIT_FAILURE);
+ }
+
+ /* Exit cleanup hook to restore normal terminal */
+ (void)atexit(cleanup);
+
+ /*
+ * XXX Useful but relies on an ncurses extension, also supported by
+ * NetBSD curses. init_pair() cannot accept -1 according to X/Open.
+ * But this special value means to keep the default foreground or
+ * background color of the terminal configuration. This also permits
+ * enforced color pairs to really only define the foreground or
+ * background color.
+ */
+ (void)use_default_colors();
+ if (start_color() == ERR)
+ err(EXIT_FAILURE, "start_color()");
+ (void)init_pair(0, -1, COLOR_BLACK);
+ (void)init_pair(1, -1, COLOR_RED);
+ (void)init_pair(2, -1, COLOR_GREEN);
+ (void)init_pair(3, -1, COLOR_YELLOW);
+ (void)init_pair(4, -1, COLOR_BLUE);
+ (void)init_pair(5, -1, COLOR_MAGENTA);
+ (void)init_pair(6, -1, COLOR_CYAN);
+ (void)init_pair(7, -1, COLOR_WHITE);
+
+ (void)cbreak(); /* Use raw() if not wanting SIGINT */
+ (void)noecho();
+ (void)keypad(w, TRUE);
+ (void)nonl();
+ (void)intrflush(stdscr, FALSE);
+ (void)timeout(0);
+
+ /* Setup animation world */
+ anim_init();
+
+ /* Initialize screen */
+ (void)erase();
+ (void)doupdate();
+ (void)curs_set(0);
+
+ /* Animate until user requests to quit with q */
+ anim_redraw();
+ for (;;) {
+ int x = -1, y = -1, c = -1, uc = -1;
+
+ /* Sleep for most of the frame */
+ (void)usleep(1000000 / ANIM_FPS);
+
+ /* Non-blocking check for user input */
+ if ((c = wgetch(w)) != ERR) {
+ uc = c;
+ /*
+ * If we don't flush and keyboard input/repeat is
+ * faster than our refresh rate, the input queue grows
+ * meaning that the avatar takes a while to react to
+ * immediate user input.
+ */
+ (void)flushinp();
+ }
+
+ /* Process user input if any */
+ switch (uc) {
+ case -1:
+ break;
+ case 'q':
+ goto end;
+ break;
+
+ case KEY_UP:
+ case '8': /* FALLTHROUGH */
+ case 'i': /* FALLTHROUGH */
+ case 'a': /* FALLTHROUGH */
+ if (avatar.y > 0) {
+ y = avatar.y - 1;
+ x = avatar.x;
+ }
+ break;
+
+ case KEY_DOWN:
+ case '2': /* FALLTHROUGH */
+ case 'k': /* FALLTHROUGH */
+ case 'm': /* FALLTHROUGH */
+ case 'z': /* FALLTHROUGH */
+ if (avatar.y < TERM_LINES - 1) {
+ y = avatar.y + 1;
+ x = avatar.x;
+ }
+ break;
+
+ case KEY_LEFT:
+ case '4': /* FALLTHROUGH */
+ case 'j': /* FALLTHROUGH */
+ case ',': /* FALLTHROUGH */
+ if (avatar.x > 0) {
+ x = avatar.x - 1;
+ y = avatar.y;
+ }
+ break;
+
+ case KEY_RIGHT:
+ case '6': /* FALLTHROUGH */
+ case 'l': /* FALLTHROUGH */
+ case '.': /* FALLTHROUGH */
+ if (avatar.x < TERM_COLUMNS - 1) {
+ x = avatar.x + 1;
+ y = avatar.y;
+ }
+ break;
+ }
+
+ /* Update avatar if necessary */
+ if (x != -1 && y != -1) {
+ avatar.x = x;
+ avatar.y = y;
+ }
+
+ /* Animate */
+ anim_update();
+ anim_redraw();
+ uc = -1;
+ }
+end:
+
+ exit(EXIT_SUCCESS);
+}
+
+static void
+cleanup(void)
+{
+
+ (void)curs_set(1);
+ (void)endwin();
+}
+
+static void
+anim_init(void)
+{
+ int i, col;
+
+ srandom(13);
+ col = 0;
+ for (i = 0; i < NOBJECTS; i++) {
+ aobject_t *o = &objects[i];
+
+ if (i <= 26) {
+ o->t = AOT_BUG;
+ o->c = 'A' + i;
+ o->col = ++col % 7;
+ } else {
+ o->t = AOT_STAR;
+ o->c = '.';
+ o->col = 0;
+ }
+ o->x = random() % (TERM_COLUMNS - 1);
+ o->y = random() % (TERM_LINES - 1);
+ }
+}
+
+static void
+anim_redraw(void)
+{
+ int i;
+
+ (void)attrset(COLOR_PAIR(0));
+ (void)erase();
+ for (i = 0; i < NOBJECTS; i++) {
+ aobject_t *o = &objects[i];
+
+ (void)attrset(COLOR_PAIR(o->col));
+ (void)mvaddch(o->y, o->x, o->c);
+ }
+
+ /* Avatar */
+ (void)attr_on(A_REVERSE, NULL);
+ (void)mvaddch(avatar.y, avatar.x, avatar.c);
+ (void)attr_off(A_REVERSE, NULL);
+
+ (void)doupdate();
+}
+
+static void
+anim_update(void)
+{
+ int i;
+
+ for (i = 0; i < NOBJECTS; i++) {
+ aobject_t *o = &objects[i];
+ int x = o->x, y = o->y, d = random() % 4;
+
+ if (o->t == AOT_STAR) {
+ if (x > 0)
+ x--;
+ else
+ x = TERM_COLUMNS - 1;
+ } else {
+ switch (d) {
+ case 0: /* Up */
+ if (y > 0)
+ y--;
+ break;
+ case 1: /* Down */
+ if (y < TERM_LINES - 1)
+ y++;
+ break;
+ case 2: /* Left */
+ if (x > 0)
+ x--;
+ break;
+ case 3: /* Right */
+ if (x < TERM_COLUMNS - 1)
+ x++;
+ break;
+ }
+ }
+ o->x = x;
+ o->y = y;
+ }
+}