root/games/cgram/cgram.c
/* $NetBSD: cgram.c,v 1.30 2023/05/10 12:30:27 rillig Exp $ */

/*-
 * Copyright (c) 2013, 2021 The NetBSD Foundation, Inc.
 * All rights reserved.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Roland Illig.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/cdefs.h>
#if defined(__RCSID) && !defined(lint)
__RCSID("$NetBSD: cgram.c,v 1.30 2023/05/10 12:30:27 rillig Exp $");
#endif

#include <assert.h>
#include <ctype.h>
#include <curses.h>
#include <err.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "pathnames.h"


static bool
ch_islower(char ch)
{
        return ch >= 'a' && ch <= 'z';
}

static bool
ch_isupper(char ch)
{
        return ch >= 'A' && ch <= 'Z';
}

static bool
ch_isalpha(char ch)
{
        return ch_islower(ch) || ch_isupper(ch);
}

static char
ch_toupper(char ch)
{
        return ch_islower(ch) ? (char)(ch - 'a' + 'A') : ch;
}

static char
ch_tolower(char ch)
{
        return ch_isupper(ch) ? (char)(ch - 'A' + 'a') : ch;
}

static int
imax(int a, int b)
{
        return a > b ? a : b;
}

static int
imin(int a, int b)
{
        return a < b ? a : b;
}

////////////////////////////////////////////////////////////

struct string {
        char *s;
        size_t len;
        size_t cap;
};

struct stringarray {
        struct string *v;
        size_t num;
};

static void
string_init(struct string *s)
{
        s->s = NULL;
        s->len = 0;
        s->cap = 0;
}

static void
string_add(struct string *s, char ch)
{
        if (s->len >= s->cap) {
                s->cap = 2 * s->cap + 16;
                s->s = realloc(s->s, s->cap);
                if (s->s == NULL)
                        errx(1, "Out of memory");
        }
        s->s[s->len++] = ch;
}

static void
string_finish(struct string *s)
{
        string_add(s, '\0');
        s->len--;
}

static void
stringarray_init(struct stringarray *a)
{
        a->v = NULL;
        a->num = 0;
}

static void
stringarray_done(struct stringarray *a)
{
        for (size_t i = 0; i < a->num; i++)
                free(a->v[i].s);
        free(a->v);
}

static void
stringarray_add(struct stringarray *a, struct string *s)
{
        size_t num = a->num++;
        if (reallocarr(&a->v, a->num, sizeof(a->v[0])) != 0)
                errx(1, "Out of memory");
        a->v[num] = *s;
}

static void
stringarray_dup(struct stringarray *dst, const struct stringarray *src)
{
        assert(dst->num == 0);
        for (size_t i = 0; i < src->num; i++) {
                struct string str;
                string_init(&str);
                for (const char *p = src->v[i].s; *p != '\0'; p++)
                        string_add(&str, *p);
                string_finish(&str);
                stringarray_add(dst, &str);
        }
}

////////////////////////////////////////////////////////////

static struct stringarray lines;
static struct stringarray sollines;
static bool hinting;
static int extent_x;
static int extent_y;
static int offset_x;
static int offset_y;
static int cursor_x;
static int cursor_y;

static int
cur_max_x(void)
{
        return (int)lines.v[cursor_y].len;
}

static int
cur_max_y(void)
{
        return extent_y - 1;
}

static char
char_left_of_cursor(void)
{
        if (cursor_x > 0)
                return lines.v[cursor_y].s[cursor_x - 1];
        assert(cursor_y > 0);
        return '\n'; /* eol of previous line */
}

static char
char_at_cursor(void)
{
        if (cursor_x == cur_max_x())
                return '\n';
        return lines.v[cursor_y].s[cursor_x];
}

static void
getquote(FILE *f)
{
        struct string line;
        string_init(&line);

        int ch;
        while ((ch = fgetc(f)) != EOF) {
                if (ch == '\n') {
                        string_finish(&line);
                        stringarray_add(&lines, &line);
                        string_init(&line);
                } else if (ch == '\t') {
                        string_add(&line, ' ');
                        while (line.len % 8 != 0)
                                string_add(&line, ' ');
                } else if (ch == '\b') {
                        if (line.len > 0)
                                line.len--;
                } else {
                        string_add(&line, (char)ch);
                }
        }

        stringarray_dup(&sollines, &lines);

        extent_y = (int)lines.num;
        for (int i = 0; i < extent_y; i++)
                extent_x = imax(extent_x, (int)lines.v[i].len);
}

static void
readfile(const char *name)
{
        FILE *f = fopen(name, "r");
        if (f == NULL)
                err(1, "%s", name);

        getquote(f);

        if (fclose(f) != 0)
                err(1, "%s", name);
}


static void
readquote(void)
{
        FILE *f = popen(_PATH_FORTUNE, "r");
        if (f == NULL)
                err(1, "%s", _PATH_FORTUNE);

        getquote(f);

        if (pclose(f) != 0)
                exit(1); /* error message must come from child process */
}

static void
encode(void)
{
        int key[26];

        for (int i = 0; i < 26; i++)
                key[i] = i;

        for (int i = 26; i > 1; i--) {
                int c = (int)(random() % i);
                int t = key[i - 1];
                key[i - 1] = key[c];
                key[c] = t;
        }

        for (int y = 0; y < extent_y; y++) {
                for (char *p = lines.v[y].s; *p != '\0'; p++) {
                        if (ch_islower(*p))
                                *p = (char)('a' + key[*p - 'a']);
                        if (ch_isupper(*p))
                                *p = (char)('A' + key[*p - 'A']);
                }
        }
}

static void
substitute(char a, char b)
{
        char la = ch_tolower(a);
        char ua = ch_toupper(a);
        char lb = ch_tolower(b);
        char ub = ch_toupper(b);

        for (int y = 0; y < (int)lines.num; y++) {
                for (char *p = lines.v[y].s; *p != '\0'; p++) {
                        if (*p == la)
                                *p = lb;
                        else if (*p == ua)
                                *p = ub;
                        else if (*p == lb)
                                *p = la;
                        else if (*p == ub)
                                *p = ua;
                }
        }
}

static bool
is_solved(void)
{
        for (size_t i = 0; i < lines.num; i++)
                if (strcmp(lines.v[i].s, sollines.v[i].s) != 0)
                        return false;
        return true;
}

////////////////////////////////////////////////////////////

static void
redraw(void)
{
        erase();

        int max_y = imin(LINES - 1, extent_y - offset_y);
        for (int y = 0; y < max_y; y++) {
                move(y, 0);

                int len = (int)lines.v[offset_y + y].len;
                int max_x = imin(COLS - 1, len - offset_x);
                const char *line = lines.v[offset_y + y].s;
                const char *solline = sollines.v[offset_y + y].s;

                for (int x = 0; x < max_x; x++) {
                        char ch = line[offset_x + x];
                        bool bold = hinting &&
                            (ch == solline[offset_x + x] || !ch_isalpha(ch));

                        if (bold)
                                attron(A_BOLD);
                        addch(ch);
                        if (bold)
                                attroff(A_BOLD);
                }
                clrtoeol();
        }

        move(LINES - 1, 0);
        addstr("~ to quit, * to cheat, ^pnfb to move");

        if (is_solved()) {
                if (extent_y + 1 - offset_y < LINES - 2)
                        move(extent_y + 1 - offset_y, 0);
                else
                        addch(' ');
                attron(A_BOLD | A_STANDOUT);
                addstr("*solved*");
                attroff(A_BOLD | A_STANDOUT);
        }

        move(cursor_y - offset_y, cursor_x - offset_x);

        refresh();
}

////////////////////////////////////////////////////////////

static void
saturate_cursor(void)
{
        cursor_y = imax(cursor_y, 0);
        cursor_y = imin(cursor_y, cur_max_y());

        assert(cursor_x >= 0);
        cursor_x = imin(cursor_x, cur_max_x());
}

static void
scroll_into_view(void)
{
        if (cursor_x < offset_x)
                offset_x = cursor_x;
        if (cursor_x > offset_x + COLS - 1)
                offset_x = cursor_x - (COLS - 1);

        if (cursor_y < offset_y)
                offset_y = cursor_y;
        if (cursor_y > offset_y + LINES - 2)
                offset_y = cursor_y - (LINES - 2);
}

static bool
can_go_left(void)
{
        return cursor_y > 0 ||
            (cursor_y == 0 && cursor_x > 0);
}

static bool
can_go_right(void)
{
        return cursor_y < cur_max_y() ||
            (cursor_y == cur_max_y() && cursor_x < cur_max_x());
}

static void
go_to_prev_line(void)
{
        cursor_y--;
        cursor_x = cur_max_x();
}

static void
go_to_next_line(void)
{
        cursor_x = 0;
        cursor_y++;
}

static void
go_left(void)
{
        if (cursor_x > 0)
                cursor_x--;
        else if (cursor_y > 0)
                go_to_prev_line();
}

static void
go_right(void)
{
        if (cursor_x < cur_max_x())
                cursor_x++;
        else if (cursor_y < cur_max_y())
                go_to_next_line();
}

static void
go_to_prev_word(void)
{
        while (can_go_left() && !ch_isalpha(char_left_of_cursor()))
                go_left();

        while (can_go_left() && ch_isalpha(char_left_of_cursor()))
                go_left();
}

static void
go_to_next_word(void)
{
        while (can_go_right() && ch_isalpha(char_at_cursor()))
                go_right();

        while (can_go_right() && !ch_isalpha(char_at_cursor()))
                go_right();
}

static bool
can_substitute_here(int ch)
{
        return isascii(ch) &&
            ch_isalpha((char)ch) &&
            cursor_x < cur_max_x() &&
            ch_isalpha(char_at_cursor());
}

static void
handle_char_input(int ch)
{
        if (ch == char_at_cursor())
                go_right();
        else if (can_substitute_here(ch)) {
                substitute(char_at_cursor(), (char)ch);
                go_right();
        } else
                beep();
}

static bool
handle_key(void)
{
        int ch = getch();

#define CTRL(letter) (letter - 64)
        switch (ch) {
        case CTRL('A'):
        case KEY_BEG:
        case KEY_HOME:
                cursor_x = 0;
                break;
        case CTRL('B'):
        case KEY_LEFT:
                go_left();
                break;
        case CTRL('E'):
        case KEY_END:
                cursor_x = cur_max_x();
                break;
        case CTRL('F'):
        case KEY_RIGHT:
                go_right();
                break;
        case '\t':
                go_to_next_word();
                break;
        case KEY_BTAB:
                go_to_prev_word();
                break;
        case '\n':
                go_to_next_line();
                break;
        case CTRL('L'):
                clear();
                break;
        case CTRL('N'):
        case KEY_DOWN:
                cursor_y++;
                break;
        case CTRL('P'):
        case KEY_UP:
                cursor_y--;
                break;
        case KEY_PPAGE:
                cursor_y -= LINES - 2;
                break;
        case KEY_NPAGE:
                cursor_y += LINES - 2;
                break;
        case '*':
                hinting = !hinting;
                break;
        case '~':
                return false;
        case KEY_RESIZE:
                break;
        default:
                handle_char_input(ch);
                break;
        }
        return true;
}

static void
init(const char *filename)
{
        stringarray_init(&lines);
        stringarray_init(&sollines);
        srandom((unsigned int)time(NULL));
        if (filename != NULL) {
            readfile(filename);
        } else {
            readquote();
        }
        encode();

        initscr();
        cbreak();
        noecho();
        keypad(stdscr, true);
}

static void
loop(void)
{
        for (;;) {
                redraw();
                if (!handle_key())
                        break;
                saturate_cursor();
                scroll_into_view();
        }
}

static void
done(void)
{
        move(LINES - 1, 0);
        clrtoeol();
        refresh();

        endwin();

        stringarray_done(&sollines);
        stringarray_done(&lines);
}


static void __dead
usage(void)
{

        fprintf(stderr, "usage: %s [file]\n", getprogname());
        exit(1);
}

int
main(int argc, char *argv[])
{

        setprogname(argv[0]);
        if (argc != 1 && argc != 2)
                usage();

        init(argc > 1 ? argv[1] : NULL);
        loop();
        done();
        return 0;
}