/* Copyright (C) 2018, 2021 June McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0])) typedef unsigned uint; enum { ScoresLen = 1000 }; static struct Score { time_t date; uint score; char name[32]; } scores[ScoresLen]; static FILE *scoresOpen(const char *path) { int fd = open(path, O_RDWR | O_CREAT, 0644); if (fd < 0) err(EX_CANTCREAT, "%s", path); FILE *file = fdopen(fd, "r+"); if (!file) err(EX_CANTCREAT, "%s", path); return file; } static void scoresLock(FILE *file) { int error = flock(fileno(file), LOCK_EX); if (error) err(EX_IOERR, "flock"); } static void scoresRead(FILE *file) { memset(scores, 0, sizeof(scores)); rewind(file); fread(scores, sizeof(struct Score), ScoresLen, file); if (ferror(file)) err(EX_IOERR, "fread"); } static void scoresWrite(FILE *file) { rewind(file); fwrite(scores, sizeof(struct Score), ScoresLen, file); if (ferror(file)) err(EX_IOERR, "fwrite"); } static size_t scoresInsert(struct Score new) { if (!new.score) return ScoresLen; for (size_t i = 0; i < ScoresLen; ++i) { if (scores[i].score > new.score) continue; memmove( &scores[i + 1], &scores[i], sizeof(struct Score) * (ScoresLen - i - 1) ); scores[i] = new; return i; } return ScoresLen; } static size_t scoresAccum(struct Score acc) { if (!acc.score) return ScoresLen; for (size_t i = 0; i < ScoresLen; ++i) { if (strcmp(scores[i].name, acc.name)) continue; scores[i].date = acc.date; scores[i].score += acc.score; while (i && scores[i-1].score < scores[i].score) { acc = scores[i]; scores[i] = scores[i-1]; scores[--i] = acc; } return i; } size_t index = scoresInsert(acc); if (index < ScoresLen) return index; index = ScoresLen - 1; scores[index] = acc; return index; } static void curse(void) { initscr(); cbreak(); echo(); curs_set(1); keypad(stdscr, true); leaveok(stdscr, false); start_color(); use_default_colors(); attr_set(A_NORMAL, 0, NULL); erase(); } enum { RankWidth = 4, ScoreWidth = 10, NameWidth = 31, DateWidth = 10, BoardWidth = RankWidth + 2 + ScoreWidth + 2 + NameWidth + 2 + DateWidth, BoardY = 0, BoardX = 2, NameX = BoardX + RankWidth + 2 + ScoreWidth + 2, BoardLen = 15, }; static char board[BoardWidth + 1]; static char *boardTitle(const char *title) { snprintf( board, sizeof(board), "%*s", (int)(BoardWidth + strlen(title)) / 2, title ); return board; } static char *boardLine(void) { for (uint i = 0; i < BoardWidth; ++i) { board[i] = '='; } board[BoardWidth] = '\0'; return board; } static char *boardScore(size_t i) { struct tm *time = localtime(&scores[i].date); if (!time) err(EX_SOFTWARE, "localtime"); char date[DateWidth + 1]; strftime(date, sizeof(date), "%F", time); snprintf( board, sizeof(board), "%*zu. %*u %-*s %*s", RankWidth, 1 + i, ScoreWidth, scores[i].score, NameWidth, scores[i].name, DateWidth, date ); return board; } static void draw(const char *title, size_t new) { mvaddstr(BoardY + 0, BoardX, boardTitle(title)); mvaddstr(BoardY + 1, BoardX, boardLine()); int newY = -1; for (size_t i = 0; i < BoardLen; ++i) { if (!scores[i].score) break; if (i == new) newY = BoardY + 2 + i; attr_set(i == new ? A_BOLD : A_NORMAL, 0, NULL); mvaddstr(BoardY + 2 + i, BoardX, boardScore(i)); } if (new == ScoresLen) return; if (new >= BoardLen) { newY = BoardY + BoardLen + 5; mvaddstr(newY - 3, BoardX, boardLine()); mvaddstr(newY - 2, BoardX, boardScore(new - 2)); mvaddstr(newY - 1, BoardX, boardScore(new - 1)); attr_set(A_BOLD, 0, NULL); mvaddstr(newY, BoardX, boardScore(new)); attr_set(A_NORMAL, 0, NULL); if (new + 1 < ScoresLen && scores[new + 1].score) { mvaddstr(newY + 1, BoardX, boardScore(new + 1)); } if (new + 2 < ScoresLen && scores[new + 2].score) { mvaddstr(newY + 2, BoardX, boardScore(new + 2)); } } move(newY, NameX); } typedef uint Play(void); uint play2048(void); uint playSnake(void); uint playFreeCell(void); static const struct Game { const char *name; const char *title; const char *desc; Play *play; bool cum; } Games[] = { { "2048", "2048", "Slide and merge matching tiles", play2048, false, }, { "snake", "Snake", "Eat food before it spoils to become long", playSnake, false, }, { "freecell", "FreeCell", "Sort cards like it's 1995", playFreeCell, true, }, }; static const struct Game *menu(void) { const char *cmd = getenv("SSH_ORIGINAL_COMMAND"); for (uint i = 0; cmd && i < ARRAY_LEN(Games); ++i) { if (!strcmp(Games[i].name, cmd)) return &Games[i]; } uint game = 0; for (;;) { for (uint i = 0; i < ARRAY_LEN(Games); ++i) { attrset(i == game ? A_STANDOUT : A_NORMAL); char buf[256]; snprintf(buf, sizeof(buf), "%u. %s", 1 + i, Games[i].title); mvaddstr(1 + 3 * i, 2, buf); attrset(A_NORMAL); mvaddstr(2 + 3 * i, 2, Games[i].desc); } attrset(A_BOLD); mvaddstr(2 + 3 * ARRAY_LEN(Games), 2, "News: "); attrset(A_NORMAL); addstr("FreeCell now supports mouse!"); attrset(A_BOLD); mvaddstr(4 + 3 * ARRAY_LEN(Games), 2, "Tip: "); attrset(A_NORMAL); addstr("You can select a game directly using "); attrset(A_BOLD); addstr("ssh -t play@ascii.town "); addstr(Games[game].name); attrset(A_NORMAL); clrtoeol(); move(1 + 3 * game, 2); int ch = getch(); if (ch == ERR) ch = getch(); switch (ch) { break; case 'k': case KEY_UP: { if (game) game--; } break; case 'j': case KEY_DOWN: { if (game + 1 < ARRAY_LEN(Games)) game++; } break; case '1' ... '9': { if (ch - '1' < (int)ARRAY_LEN(Games)) game = ch - '1'; } break; case '\r': case '\n': case KEY_ENTER: { return &Games[game]; } break; case 'q': { return NULL; } break; case ERR: exit(EXIT_FAILURE); } } } static void info(void) { endwin(); printf( "This program is AGPLv3 Free Software!\n" "Code is available from .\n" ); } int main(int argc, char *argv[]) { setlocale(LC_CTYPE, "en_US.UTF-8"); const char *path = NULL; for (int opt; 0 < (opt = getopt(argc, argv, "t:"));) { switch (opt) { break; case 't': path = optarg; break; default: return EX_USAGE; } } if (path) { FILE *file = fopen(path, "r"); if (!file) err(EX_NOINPUT, "%s", path); scoresRead(file); printf("%s\n", boardTitle("TOP SCORES")); printf("%s\n", boardLine()); for (size_t i = 0; i < ScoresLen; ++i) { if (!scores[i].score) break; printf("%s\n", boardScore(i)); } return EX_OK; } if (!isatty(STDOUT_FILENO)) { errx(EX_USAGE, "not a tty; use ssh -t"); } curse(); atexit(info); #ifdef __OpenBSD__ int error = unveil(".", "rwc"); if (error) err(EX_OSERR, "unveil"); error = pledge("stdio tty rpath wpath cpath flock", NULL); if (error) err(EX_OSERR, "pledge"); #endif const struct Game *game = menu(); if (!game) return 0; erase(); #if defined(__FreeBSD__) || defined(__OpenBSD__) setproctitle("%s", game->name); #endif char buf[256]; snprintf(buf, sizeof(buf), "%s.scores", game->name); FILE *top = scoresOpen(buf); snprintf(buf, sizeof(buf), "%s.weekly", game->name); FILE *weekly = scoresOpen(buf); #ifdef __OpenBSD__ error = pledge("stdio tty flock", NULL); if (error) err(EX_OSERR, "pledge"); #endif struct Score new = { .date = time(NULL), .score = game->play(), }; curse(); scoresRead(weekly); size_t index = scoresInsert(new); if (game->cum && index == ScoresLen && new.score) { index = ScoresLen - 1; scores[index] = new; } draw("WEEKLY SCORES", index); if (index < ScoresLen) { attr_set(A_BOLD, 0, NULL); while (!new.name[0]) { int y, x; getyx(stdscr, y, x); getnstr(new.name, sizeof(new.name) - 1); move(y, x); } for (char *ch = new.name; *ch; ++ch) { if (*ch < ' ') *ch = ' '; } scoresLock(weekly); scoresRead(weekly); if (game->cum) { index = scoresAccum(new); } else { index = scoresInsert(new); } scoresWrite(weekly); fclose(weekly); } noecho(); curs_set(0); erase(); draw("WEEKLY SCORES", index); getch(); erase(); scoresRead(top); if (game->cum) { index = scoresAccum(new); } else { index = scoresInsert(new); } draw("TOP SCORES", index); if (index < ScoresLen) { scoresLock(top); scoresRead(top); if (game->cum) { scoresAccum(new); } else { scoresInsert(new); } scoresWrite(top); fclose(top); } getch(); }