root/games/gomoku/main.c
/*      $NetBSD: main.c,v 1.73 2022/05/29 21:02:37 rillig Exp $ */

/*
 * Copyright (c) 1994
 *      The Regents of the University of California.  All rights reserved.
 *
 * This code is derived from software contributed to Berkeley by
 * Ralph Campbell.
 *
 * 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.
 * 3. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 REGENTS 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>
__COPYRIGHT("@(#) Copyright (c) 1994\
 The Regents of the University of California.  All rights reserved.");
/*      @(#)main.c      8.4 (Berkeley) 5/4/95   */
__RCSID("$NetBSD: main.c,v 1.73 2022/05/29 21:02:37 rillig Exp $");

#include <sys/stat.h>
#include <curses.h>
#include <err.h>
#include <limits.h>
#include <signal.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "gomoku.h"

enum input_source {
        USER,                   /* get input from standard input */
        PROGRAM,                /* get input from program */
        INPUTF                  /* get input from a file */
};

enum testing_mode {
        NORMAL_PLAY,
        USER_VS_USER,
        PROGRAM_VS_PROGRAM
};

bool    interactive = true;     /* true if interactive */
int     debug;                  /* > 0 if debugging */
static enum testing_mode test = NORMAL_PLAY;
static char *prog;              /* name of program */
static char user[LOGIN_NAME_MAX]; /* name of player */
static FILE *debugfp;           /* file for debug output */
static FILE *inputfp;           /* file for debug input */

const char      pdir[4]         = "-\\|/";

struct  spotstr board[BAREA];           /* info for board */
struct  combostr frames[FAREA];         /* storage for all frames */
struct  combostr *sortframes[2];        /* sorted list of non-empty frames */
u_char  overlap[FAREA * FAREA];         /* non-zero if frame [a][b] overlap;
                                         * see init_overlap */
spot_index intersect[FAREA * FAREA];    /* frame [a][b] intersection */
struct game game;
const char *plyr[2] = { "???", "???" }; /* who's who */

static spot_index readinput(FILE *);
static void misclog(const char *, ...) __printflike(1, 2);
static void quit(void) __dead;
#if !defined(DEBUG)
static void quitsig(int) __dead;
#endif

static void
warn_if_exists(const char *fname)
{
        struct stat st;

        if (lstat(fname, &st) == 0) {
                int x, y;
                getyx(stdscr, y, x);
                addstr("  (already exists)");
                move(y, x);
        } else
                clrtoeol();
}

static void
save_game(void)
{
        char fname[PATH_MAX];
        FILE *fp;

        ask("Save file name? ");
        (void)get_line(fname, sizeof(fname), warn_if_exists);
        if ((fp = fopen(fname, "w")) == NULL) {
                misclog("cannot create save file");
                return;
        }
        for (unsigned int m = 0; m < game.nmoves; m++)
                fprintf(fp, "%s\n", stoc(game.moves[m]));
        fclose(fp);
}

static void
parse_args(int argc, char **argv)
{
        int ch;

        prog = strrchr(argv[0], '/');
        prog = prog != NULL ? prog + 1 : argv[0];

        while ((ch = getopt(argc, argv, "bcdD:u")) != -1) {
                switch (ch) {
                case 'b':       /* background */
                        interactive = false;
                        break;
                case 'c':
                        test = PROGRAM_VS_PROGRAM;
                        break;
                case 'd':
                        debug++;
                        break;
                case 'D':       /* log debug output to file */
                        if ((debugfp = fopen(optarg, "w")) == NULL)
                                err(1, "%s", optarg);
                        break;
                case 'u':
                        test = USER_VS_USER;
                        break;
                default:
                usage:
                        fprintf(stderr, "usage: %s [-bcdu] [-Dfile] [file]\n",
                            getprogname());
                        exit(EXIT_FAILURE);
                }
        }
        argc -= optind;
        argv += optind;
        if (argc > 1)
                goto usage;
        if (argc == 1 && (inputfp = fopen(*argv, "r")) == NULL)
                err(1, "%s", *argv);
}

static void
set_input_sources(enum input_source *input, player_color color)
{
        switch (test) {
        case NORMAL_PLAY:
                input[color] = USER;
                input[color != BLACK ? BLACK : WHITE] = PROGRAM;
                break;
        case USER_VS_USER:
                input[BLACK] = USER;
                input[WHITE] = USER;
                break;
        case PROGRAM_VS_PROGRAM:
                input[BLACK] = PROGRAM;
                input[WHITE] = PROGRAM;
                break;
        }
}

static int
ask_user_color(void)
{
        int color;

        mvprintw(BSZ + 3, 0, "Black moves first. ");
        ask("(B)lack or (W)hite? ");
        for (;;) {
                int ch = get_key(NULL);
                if (ch == 'b' || ch == 'B') {
                        color = BLACK;
                        break;
                }
                if (ch == 'w' || ch == 'W') {
                        color = WHITE;
                        break;
                }
                if (ch == 'q' || ch == 'Q')
                        quit();

                beep();
                ask("Please choose (B)lack or (W)hite: ");
        }
        move(BSZ + 3, 0);
        clrtoeol();
        return color;
}

static int
read_color(void)
{
        char buf[128];

        get_line(buf, sizeof(buf), NULL);
        if (strcmp(buf, "black") == 0)
                return BLACK;
        if (strcmp(buf, "white") == 0)
                return WHITE;
        panic("Huh?  Expected `black' or `white', got `%s'\n", buf);
        /* NOTREACHED */
}

static spot_index
read_move(void)
{
again:
        if (interactive) {
                ask("Select move, (S)ave or (Q)uit.");
                spot_index s = get_coord();
                if (s == SAVE) {
                        save_game();
                        goto again;
                }
                if (s != RESIGN && board[s].s_occ != EMPTY) {
                        beep();
                        goto again;
                }
                return s;
        } else {
                char buf[128];
                if (!get_line(buf, sizeof(buf), NULL))
                        return RESIGN;
                if (buf[0] == '\0')
                        goto again;
                return ctos(buf);
        }
}

static void
declare_winner(int outcome, const enum input_source *input, player_color color)
{

        move(BSZ + 3, 0);
        switch (outcome) {
        case WIN:
                if (input[color] == PROGRAM)
                        addstr("Ha ha, I won");
                else if (input[0] == USER && input[1] == USER)
                        addstr("Well, you won (and lost)");
                else
                        addstr("Rats! you won");
                break;
        case TIE:
                addstr("Wow! It's a tie");
                break;
        case ILLEGAL:
                addstr("Illegal move");
                break;
        }
        clrtoeol();
        bdisp();
}

struct outcome {
        int result;
        player_color winner;
};

static struct outcome
main_game_loop(enum input_source *input)
{
        spot_index curmove = 0;
        player_color color = BLACK;

again:
        switch (input[color]) {
        case INPUTF:
                curmove = readinput(inputfp);
                if (curmove != END_OF_INPUT)
                        break;
                set_input_sources(input, color);
                plyr[BLACK] = input[BLACK] == USER ? user : prog;
                plyr[WHITE] = input[WHITE] == USER ? user : prog;
                bdwho();
                refresh();
                goto again;

        case USER:
                curmove = read_move();
                break;

        case PROGRAM:
                if (interactive)
                        ask("Thinking...");
                curmove = pickmove(color);
                break;
        }

        if (interactive && curmove != ILLEGAL) {
                misclog("%3u%*s%-6s",
                    game.nmoves + 1, color == BLACK ? 2 : 9, "",
                    stoc(curmove));
        }

        int outcome;
        if ((outcome = makemove(color, curmove)) != MOVEOK)
                return (struct outcome){ outcome, color };

        if (interactive)
                bdisp();
        color = color != BLACK ? BLACK : WHITE;
        goto again;
}

int
main(int argc, char **argv)
{
        char *user_name;
        int color;
        enum input_source input[2];

        /* Revoke setgid privileges */
        setgid(getgid());

        setprogname(argv[0]);

        user_name = getlogin();
        strlcpy(user, user_name != NULL ? user_name : "you", sizeof(user));

        color = BLACK;

        parse_args(argc, argv);

        if (debug == 0)
                srandom((unsigned int)time(0));
        if (interactive)
                cursinit();             /* initialize curses */
again:
        init_board();                   /* initialize board contents */

        if (interactive) {
                bdisp_init();           /* initialize display of board */
#ifdef DEBUG
                signal(SIGINT, whatsup);
#else
                signal(SIGINT, quitsig);
#endif

                if (inputfp == NULL && test == NORMAL_PLAY)
                        color = ask_user_color();
        } else {
                setbuf(stdout, NULL);
                color = read_color();
        }

        if (inputfp != NULL) {
                input[BLACK] = INPUTF;
                input[WHITE] = INPUTF;
        } else {
                set_input_sources(input, color);
        }
        if (interactive) {
                plyr[BLACK] = input[BLACK] == USER ? user : prog;
                plyr[WHITE] = input[WHITE] == USER ? user : prog;
                bdwho();
                refresh();
        }

        struct outcome outcome = main_game_loop(input);

        if (interactive) {
                declare_winner(outcome.result, input, outcome.winner);
                if (outcome.result != RESIGN) {
                replay:
                        ask("Play again? ");
                        int ch = get_key("YyNnQqSs");
                        if (ch == 'Y' || ch == 'y')
                                goto again;
                        if (ch == 'S' || ch == 's') {
                                save_game();
                                goto replay;
                        }
                }
        }
        quit();
}

static spot_index
readinput(FILE *fp)
{
        int c;
        char buf[128];
        size_t pos;

        pos = 0;
        while ((c = getc(fp)) != EOF && c != '\n' && pos < sizeof(buf) - 1)
                buf[pos++] = c;
        buf[pos] = '\0';
        return c == EOF ? END_OF_INPUT : ctos(buf);
}

#ifdef DEBUG

static bool
skip_any(const char **pp, const char *s)
{
        while (strchr(s, **pp) != NULL)
                (*pp)++;
        return true;
}

static bool
parse_char_index(const char **pp, const char *s, unsigned int *out)
{
        const char *found = strchr(s, **pp);
        if (found != NULL)
                *out = (unsigned int)(found - s), (*pp)++;
        return found != NULL;
}

static bool
parse_direction(const char **pp, direction *out)
{
        unsigned int u;
        if (!parse_char_index(pp, "-\\|/", &u))
                return false;
        *out = (direction)u;
        return true;
}

static bool
parse_row(const char **pp, unsigned int *out)
{
        if (!('0' <= **pp && **pp <= '9'))
                return false;
        unsigned int u = *(*pp)++ - '0';
        if ('0' <= **pp && **pp <= '9')
                u = 10 * u + *(*pp)++ - '0';
        *out = u;
        return 1 <= u && u <= BSZ;
}

static bool
parse_spot(const char **pp, spot_index *out)
{
        unsigned row, col;
        if (!parse_char_index(pp, "abcdefghjklmnopqrst", &col) &&
            !parse_char_index(pp, "ABCDEFGHJKLMNOPQRST", &col))
                return false;
        if (!parse_row(pp, &row))
                return false;
        *out = PT(col + 1, row);
        return true;
}

/*
 * Handle strange situations and ^C.
 */
/* ARGSUSED */
void
whatsup(int signum __unused)
{
        unsigned int n;
        player_color color;
        spot_index s, s1, s2;
        direction r1, r2;
        struct spotstr *sp;
        FILE *fp;
        const char *str;
        struct elist *ep;
        struct combostr *cbp;
        char input[128];
        char tmp[128];

        if (!interactive)
                quit();
top:
        ask("debug command: ");
        if (!get_line(input, sizeof(input), NULL))
                quit();
        switch (*input) {
        case '\0':
                goto top;
        case 'q':               /* conservative quit */
                quit();
                /* NOTREACHED */
        case 'd':               /* set debug level */
                debug = input[1] - '0';
                debuglog("Debug set to %d", debug);
                goto top;
        case 'c':
                ask("");
                return;
        case 'b':               /* back up a move */
                if (game.nmoves > 0) {
                        game.nmoves--;
                        board[game.moves[game.nmoves]].s_occ = EMPTY;
                        bdisp();
                }
                goto top;
        case 's':               /* suggest a move */
                color = input[1] == 'b' ? BLACK : WHITE;
                debuglog("suggest %c %s", color == BLACK ? 'B' : 'W',
                        stoc(pickmove(color)));
                goto top;
        case 'f':               /* go forward a move */
                board[game.moves[game.nmoves]].s_occ =
                    game.nmoves % 2 == 0 ? BLACK : WHITE;
                game.nmoves++;
                bdisp();
                goto top;
        case 'l':               /* print move history */
                if (input[1] == '\0') {
                        for (unsigned int m = 0; m < game.nmoves; m++)
                                debuglog("%s", stoc(game.moves[m]));
                        goto top;
                }
                if ((fp = fopen(input + 1, "w")) == NULL)
                        goto top;
                for (unsigned int m = 0; m < game.nmoves; m++) {
                        fprintf(fp, "%s", stoc(game.moves[m]));
                        if (++m < game.nmoves)
                                fprintf(fp, " %s\n", stoc(game.moves[m]));
                        else
                                fputc('\n', fp);
                }
                bdump(fp);
                fclose(fp);
                goto top;
        case 'o':
                str = input + 1;
                if (skip_any(&str, " ") &&
                    parse_spot(&str, &s1) &&
                    parse_direction(&str, &r1) &&
                    skip_any(&str, ", ") &&
                    parse_spot(&str, &s2) &&
                    parse_direction(&str, &r2) &&
                    *str == '\0') {
                        n = board[s1].s_frame[r1] * FAREA
                            + board[s2].s_frame[r2];
                        debuglog("overlap %s%c,%s%c = %02x",
                            stoc(s1), pdir[r1], stoc(s2), pdir[r2],
                            overlap[n]);
                } else
                        debuglog("usage: o <spot><dir> <spot><dir>");
                goto top;
        case 'p':
                sp = &board[s = ctos(input + 1)];
                debuglog("V %s %x/%d %d %x/%d %d %d %x", stoc(s),
                        sp->s_combo[BLACK].s, sp->s_level[BLACK],
                        sp->s_nforce[BLACK],
                        sp->s_combo[WHITE].s, sp->s_level[WHITE],
                        sp->s_nforce[WHITE], sp->s_wval, sp->s_flags);
                debuglog("FB %s %x %x %x %x", stoc(s),
                        sp->s_fval[BLACK][0].s, sp->s_fval[BLACK][1].s,
                        sp->s_fval[BLACK][2].s, sp->s_fval[BLACK][3].s);
                debuglog("FW %s %x %x %x %x", stoc(s),
                        sp->s_fval[WHITE][0].s, sp->s_fval[WHITE][1].s,
                        sp->s_fval[WHITE][2].s, sp->s_fval[WHITE][3].s);
                goto top;
        case 'e':       /* e [0-9] spot */
                str = input + 1;
                if (*str >= '0' && *str <= '9')
                        n = *str++ - '0';
                else
                        n = 0;
                sp = &board[ctos(str)];
                for (ep = sp->s_empty; ep != NULL; ep = ep->e_next) {
                        cbp = ep->e_combo;
                        if (n != 0) {
                                if (cbp->c_nframes > n)
                                        continue;
                                if (cbp->c_nframes != n)
                                        break;
                        }
                        printcombo(cbp, tmp, sizeof(tmp));
                        debuglog("%s", tmp);
                }
                goto top;
        default:
                debuglog("Options are:");
                debuglog("q    - quit");
                debuglog("c    - continue");
                debuglog("d#   - set debug level to #");
                debuglog("p#   - print values at #");
                goto top;
        }
}
#endif /* DEBUG */

/*
 * Display debug info.
 */
void
debuglog(const char *fmt, ...)
{
        va_list ap;
        char buf[128];

        va_start(ap, fmt);
        vsnprintf(buf, sizeof(buf), fmt, ap);
        va_end(ap);

        if (debugfp != NULL)
                fprintf(debugfp, "%s\n", buf);
        if (interactive)
                dislog(buf);
        else
                fprintf(stderr, "%s\n", buf);
}

static void
misclog(const char *fmt, ...)
{
        va_list ap;
        char buf[128];

        va_start(ap, fmt);
        vsnprintf(buf, sizeof(buf), fmt, ap);
        va_end(ap);

        if (debugfp != NULL)
                fprintf(debugfp, "%s\n", buf);
        if (interactive)
                dislog(buf);
        else
                printf("%s\n", buf);
}

static void
quit(void)
{
        if (interactive) {
                bdisp();                /* show final board */
                cursfini();
        }
        exit(0);
}

#if !defined(DEBUG)
static void
quitsig(int dummy __unused)
{
        quit();
}
#endif

/*
 * Die gracefully.
 */
void
panic(const char *fmt, ...)
{
        va_list ap;

        if (interactive) {
                bdisp();
                cursfini();
        }

        fprintf(stderr, "%s: ", prog);
        va_start(ap, fmt);
        vfprintf(stderr, fmt, ap);
        va_end(ap);
        fprintf(stderr, "\n");

        fputs("I resign\n", stdout);
        exit(1);
}