2048 over SSH https://ascii.town
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

271 lines
6.1KB

  1. /* Copyright (C) 2018 C. McEnroe <june@causal.agency>
  2. *
  3. * This program is free software: you can redistribute it and/or modify
  4. * it under the terms of the GNU Affero General Public License as published by
  5. * the Free Software Foundation, either version 3 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU Affero General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU Affero General Public License
  14. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. */
  16. #include <curses.h>
  17. #include <err.h>
  18. #include <fcntl.h>
  19. #include <stdbool.h>
  20. #include <stdio.h>
  21. #include <stdlib.h>
  22. #include <string.h>
  23. #include <sys/file.h>
  24. #include <sysexits.h>
  25. #include <time.h>
  26. #include <unistd.h>
  27. #ifdef __FreeBSD__
  28. #include <sys/capsicum.h>
  29. #endif
  30. typedef unsigned uint;
  31. uint play2048(void);
  32. enum { ScoresLen = 1000 };
  33. static struct Score {
  34. time_t date;
  35. uint score;
  36. char name[32];
  37. } scores[ScoresLen];
  38. static FILE *scoresOpen(const char *path) {
  39. int fd = open(path, O_RDWR | O_CREAT, 0644);
  40. if (fd < 0) err(EX_CANTCREAT, "%s", path);
  41. FILE *file = fdopen(fd, "r+");
  42. if (!file) err(EX_CANTCREAT, "%s", path);
  43. return file;
  44. }
  45. static void scoresLock(FILE *file) {
  46. int error = flock(fileno(file), LOCK_EX);
  47. if (error) err(EX_IOERR, "flock");
  48. }
  49. static void scoresRead(FILE *file) {
  50. memset(scores, 0, sizeof(scores));
  51. rewind(file);
  52. fread(scores, sizeof(struct Score), ScoresLen, file);
  53. if (ferror(file)) err(EX_IOERR, "fread");
  54. }
  55. static void scoresWrite(FILE *file) {
  56. rewind(file);
  57. fwrite(scores, sizeof(struct Score), ScoresLen, file);
  58. if (ferror(file)) err(EX_IOERR, "fwrite");
  59. }
  60. static size_t scoresInsert(struct Score new) {
  61. if (!new.score) return ScoresLen;
  62. for (size_t i = 0; i < ScoresLen; ++i) {
  63. if (scores[i].score > new.score) continue;
  64. memmove(
  65. &scores[i + 1], &scores[i],
  66. sizeof(struct Score) * (ScoresLen - i - 1)
  67. );
  68. scores[i] = new;
  69. return i;
  70. }
  71. return ScoresLen;
  72. }
  73. static void curse(void) {
  74. initscr();
  75. cbreak();
  76. echo();
  77. curs_set(1);
  78. keypad(stdscr, true);
  79. leaveok(stdscr, false);
  80. start_color();
  81. use_default_colors();
  82. attr_set(A_NORMAL, 0, NULL);
  83. erase();
  84. }
  85. enum {
  86. RankWidth = 4,
  87. ScoreWidth = 10,
  88. NameWidth = 31,
  89. DateWidth = 10,
  90. BoardWidth = RankWidth + 2 + ScoreWidth + 2 + NameWidth + 2 + DateWidth,
  91. BoardY = 0,
  92. BoardX = 2,
  93. NameX = BoardX + RankWidth + 2 + ScoreWidth + 2,
  94. BoardLen = 15,
  95. };
  96. static char board[BoardWidth + 1];
  97. static char *boardTitle(const char *title) {
  98. snprintf(
  99. board, sizeof(board),
  100. "%*s",
  101. (int)(BoardWidth + strlen(title)) / 2, title
  102. );
  103. return board;
  104. }
  105. static char *boardLine(void) {
  106. for (uint i = 0; i < BoardWidth; ++i) {
  107. board[i] = '=';
  108. }
  109. board[BoardWidth] = '\0';
  110. return board;
  111. }
  112. static char *boardScore(size_t i) {
  113. struct tm *time = localtime(&scores[i].date);
  114. if (!time) err(EX_SOFTWARE, "localtime");
  115. char date[DateWidth + 1];
  116. strftime(date, sizeof(date), "%F", time);
  117. snprintf(
  118. board, sizeof(board),
  119. "%*zu. %*u %-*s %*s",
  120. RankWidth, 1 + i,
  121. ScoreWidth, scores[i].score,
  122. NameWidth, scores[i].name,
  123. DateWidth, date
  124. );
  125. return board;
  126. }
  127. static void draw(const char *title, size_t new) {
  128. mvaddstr(BoardY + 0, BoardX, boardTitle(title));
  129. mvaddstr(BoardY + 1, BoardX, boardLine());
  130. int newY = -1;
  131. for (size_t i = 0; i < BoardLen; ++i) {
  132. if (!scores[i].score) break;
  133. if (i == new) newY = BoardY + 2 + i;
  134. attr_set(i == new ? A_BOLD : A_NORMAL, 0, NULL);
  135. mvaddstr(BoardY + 2 + i, BoardX, boardScore(i));
  136. }
  137. if (new == ScoresLen) return;
  138. if (new >= BoardLen) {
  139. newY = BoardY + BoardLen + 5;
  140. mvaddstr(newY - 3, BoardX, boardLine());
  141. mvaddstr(newY - 2, BoardX, boardScore(new - 2));
  142. mvaddstr(newY - 1, BoardX, boardScore(new - 1));
  143. attr_set(A_BOLD, 0, NULL);
  144. mvaddstr(newY, BoardX, boardScore(new));
  145. attr_set(A_NORMAL, 0, NULL);
  146. if (new + 1 < ScoresLen && scores[new + 1].score) {
  147. mvaddstr(newY + 1, BoardX, boardScore(new + 1));
  148. }
  149. if (new + 2 < ScoresLen && scores[new + 2].score) {
  150. mvaddstr(newY + 2, BoardX, boardScore(new + 2));
  151. }
  152. }
  153. move(newY, NameX);
  154. }
  155. int main(int argc, char *argv[]) {
  156. const char *path = NULL;
  157. int opt;
  158. while (0 < (opt = getopt(argc, argv, "t:"))) {
  159. switch (opt) {
  160. break; case 't': path = optarg;
  161. break; default: return EX_USAGE;
  162. }
  163. }
  164. if (path) {
  165. FILE *file = fopen(path, "r");
  166. if (!file) err(EX_NOINPUT, "%s", path);
  167. scoresRead(file);
  168. printf("%s\n", boardTitle("TOP SCORES"));
  169. printf("%s\n", boardLine());
  170. for (size_t i = 0; i < ScoresLen; ++i) {
  171. if (!scores[i].score) break;
  172. printf("%s\n", boardScore(i));
  173. }
  174. return EX_OK;
  175. }
  176. curse();
  177. FILE *top = scoresOpen("2048.scores");
  178. FILE *weekly = scoresOpen("2048.weekly");
  179. #ifdef __FreeBSD__
  180. int error = cap_enter();
  181. if (error) err(EX_OSERR, "cap_enter");
  182. cap_rights_t rights;
  183. cap_rights_init(&rights, CAP_READ, CAP_WRITE, CAP_SEEK, CAP_FLOCK);
  184. error = cap_rights_limit(fileno(top), &rights);
  185. if (error) err(EX_OSERR, "cap_rights_limit");
  186. error = cap_rights_limit(fileno(weekly), &rights);
  187. if (error) err(EX_OSERR, "cap_rights_limit");
  188. #endif
  189. struct Score new = {
  190. .date = time(NULL),
  191. .score = play2048(),
  192. };
  193. curse();
  194. scoresRead(weekly);
  195. size_t index = scoresInsert(new);
  196. draw("WEEKLY SCORES", index);
  197. if (index < ScoresLen) {
  198. attr_set(A_BOLD, 0, NULL);
  199. while (!new.name[0]) {
  200. int y, x;
  201. getyx(stdscr, y, x);
  202. getnstr(new.name, sizeof(new.name) - 1);
  203. move(y, x);
  204. }
  205. for (char *ch = new.name; *ch; ++ch) {
  206. if (*ch < ' ') *ch = ' ';
  207. }
  208. scoresLock(weekly);
  209. scoresRead(weekly);
  210. scoresInsert(new);
  211. scoresWrite(weekly);
  212. fclose(weekly);
  213. }
  214. noecho();
  215. curs_set(0);
  216. getch();
  217. erase();
  218. scoresRead(top);
  219. index = scoresInsert(new);
  220. draw("TOP SCORES", index);
  221. if (index < ScoresLen) {
  222. scoresLock(top);
  223. scoresRead(top);
  224. scoresInsert(new);
  225. scoresWrite(top);
  226. fclose(top);
  227. }
  228. getch();
  229. endwin();
  230. printf(
  231. "This program is AGPLv3 Free Software!\n"
  232. "Code is available from <https://code.causal.agency/june/play>.\n"
  233. );
  234. }