commit 53bf7297fc90a266c23c6ff6054efb06d2cd1478
Author: rani <clagv.randomgames@gmail.com>
Date: Thu Jul 20 14:25:10 2023 +0000
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d4ed8ea
--- /dev/null
+++ b/README.md
@@ -00 +13 @@
+# Games
+
+A collection of small games. Configuration is done in the source file of each game.
diff --git a/mines/README.md b/mines/README.md
new file mode 100644
index 0000000..b845462
--- /dev/null
+++ b/mines/README.md
@@ -00 +110 @@
+# Mines
+
+Minesweeper. Left click reveals a square, right click flags it. `f` toggles "flag mode" which allows placing a flag on left click as well.
+
+## Config
+
+* `Y`: Minefield height
+* `X`: Minefield width
+* `MINES`: Number of mines
+* `HIGHLIGHT_SQUARES`: Either `true` or `false`. Enables or disables highlighting of number squares
diff --git a/mines/mines.c b/mines/mines.c
new file mode 100644
index 0000000..bc41790
--- /dev/null
+++ b/mines/mines.c
@@ -00 +1405 @@
+#include <stdlib.h>
+#include <stdbool.h>
+#include <time.h>
+#include <ncurses.h>
+#include <string.h>
+
+
+/* BEGIN CONFIG */
+// grid size
+#define Y 16
+#define X 16
+
+// number of mines
+#define MINES 35
+
+// reverse the highlighting of squares
+#define HIGHLIGHT_SQUARES true
+/* END CONFIG */
+
+
+#define ARRLEN(a) (sizeof(a)/sizeof(*a))
+
+
+enum color {
+ RED = 1,
+ GREEN,
+ CYAN,
+ BLUE,
+ MAGENTA,
+ BLACK,
+};
+
+
+enum state {
+ RUNNING,
+ QUIT,
+ GAME_OVER,
+ WIN,
+};
+
+
+struct square {
+ bool revealed;
+ bool mine;
+ bool flagged;
+ int neighbors;
+};
+
+
+struct {
+ int x, y;
+} around[8] = {
+ {.x = -1, .y = -1}, {.x = 0, .y = -1}, {.x = +1, .y = -1},
+ {.x = -1, .y = 0}, {.x = +1, .y = 0},
+ {.x = -1, .y = +1}, {.x = 0, .y = +1}, {.x = +1, .y = +1}
+};
+
+
+struct square grid[X][Y] = {0};
+int total_revealed = 0;
+int total_flags = 0;
+
+
+void global_reset(void) {
+ memset(grid, 0, sizeof(grid));
+ total_revealed = 0;
+ total_flags = 0;
+}
+
+
+bool won(void) {
+ return total_revealed >= X*Y - MINES;
+}
+
+
+int count_mines(int x, int y) {
+ int n = 0;
+
+ for (int i = 0; i < ARRLEN(around); i++) {
+ int nx = x + around[i].x;
+ int ny = y + around[i].y;
+ if (nx < 0 || ny < 0 || nx >= X || ny >= Y) continue;
+ if (grid[nx][ny].mine) n++;
+ }
+
+ return n;
+}
+
+
+void reveal(int x, int y) {
+ if (grid[x][y].revealed) return;
+ total_revealed++;
+ grid[x][y].revealed = true;
+}
+
+
+void reveal_blank(int x, int y) {
+ struct coord {
+ int x, y;
+ } coords[8];
+ for (int i = 0; i < ARRLEN(coords); i++) coords[i] = (struct coord){-1, -1};
+ int n = 0;
+
+ for (int i = 0; i < ARRLEN(around); i++) {
+ int nx = x + around[i].x;
+ int ny = y + around[i].y;
+ if (nx < 0 || ny < 0 || nx >= X || ny >= Y) continue;
+ struct square s = grid[nx][ny];
+ if (s.mine) continue;
+ if (s.neighbors == 0 && !s.revealed) {
+ reveal(nx, ny);
+
+ coords[n].x = nx;
+ coords[n].y = ny;
+ n++;
+ } else if (s.neighbors != 0) reveal(nx, ny);
+ }
+
+ for (int i = 0; i < n; i++) {
+ reveal_blank(coords[i].x, coords[i].y);
+ }
+}
+
+
+void create_grid(int x, int y) {
+ struct {
+ int x, y;
+ } exclude[9] = {0};
+ int n = 0;
+
+ exclude[n].x = x;
+ exclude[n].y = y;
+ n++;
+
+ // squares around the grid that mines cannot spawn in
+ for (int i = 0; i < ARRLEN(around); i++) {
+ int nx, ny;
+ nx = x + around[i].x;
+ ny = y + around[i].y;
+
+ if (nx < 0 || ny < 0 || nx >= X || ny >= Y) continue;
+ else {
+ exclude[n].x = nx;
+ exclude[n].y = ny;
+ n++;
+ }
+ }
+
+ bool done = false;
+ int tries = 0;
+ do {
+ for (int i = 0; i < MINES; i++) {
+ int mx, my;
+ bool done = false;
+ do {
+ mx = rand() % X;
+ my = rand() % Y;
+
+ bool illegal = false;
+ for (int i = 0; i < n; i++) {
+ if (exclude[i].x == mx && exclude[i].y == my) {
+ illegal = true;
+ break;
+ }
+ }
+
+ if (illegal) continue;
+
+ if (!grid[mx][my].mine) {
+ grid[mx][my].mine = true;
+ done = true;
+ }
+ } while (!done);
+ }
+
+
+ for (int x = 0; x < X; x++) {
+ for (int y = 0; y < Y; y++) {
+ grid[x][y].neighbors = count_mines(x, y);
+ }
+ }
+
+ // prevent insta-wins
+ reveal_blank(x, y);
+ if (!won()) done = true;
+ else global_reset();
+ tries++;
+
+ } while (tries < 10 && !done);
+}
+
+
+int nlen(int n) {
+ int len = 0;
+ while (n > 0) {
+ len++;
+ n /= 10;
+ }
+ return len;
+}
+
+
+int main(void) {
+ srand(time(NULL));
+
+ initscr();
+ noecho();
+ curs_set(0);
+ keypad(stdscr, TRUE);
+
+ mousemask(ALL_MOUSE_EVENTS, NULL);
+
+ use_default_colors();
+ start_color();
+ init_pair(RED, COLOR_RED, -1);
+ init_pair(GREEN, COLOR_GREEN, -1);
+ init_pair(CYAN, COLOR_CYAN, -1);
+ init_pair(BLUE, COLOR_BLUE, -1);
+ init_pair(MAGENTA, COLOR_MAGENTA, -1);
+ init_pair(BLACK, COLOR_WHITE, COLOR_BLACK);
+
+
+ while (true) {
+ bool grid_exists = false;
+ bool flag_mode = false;
+
+ clear();
+
+ time_t start_time;
+
+ enum state done = RUNNING;
+ bool display_once_more = false;
+ while (!done || display_once_more) {
+ erase();
+ // draw the board
+ for (int x = 0; x < X; x++) addstr("+ - ");
+ addstr("+\n");
+
+ for (int y = 0; y < Y; y++) {
+ for (int x = 0; x < X; x++) {
+ addstr("| ");
+ struct square s = grid[x][y];
+ if (done == GAME_OVER && s.mine) {
+ attron(COLOR_PAIR(RED) | A_REVERSE);
+ addch('*');
+ attroff(COLOR_PAIR(RED) | A_REVERSE);
+ } else if (s.flagged) {
+ int cp = COLOR_PAIR(RED);
+ char flagc = '!';
+ if (!HIGHLIGHT_SQUARES) cp |= A_REVERSE;
+ if (done == GAME_OVER) {
+ cp = COLOR_PAIR(CYAN) | A_REVERSE;
+ flagc = '?';
+ }
+ attron(cp);
+ addch(flagc);
+ attroff(cp);
+ } else if (s.revealed) {
+ int cp = 0;
+ switch (s.neighbors) {
+ case 0: cp = 0; break;
+ case 1: cp = COLOR_PAIR(CYAN); break;
+ case 2: cp = COLOR_PAIR(MAGENTA); break;
+ case 3: cp = COLOR_PAIR(BLACK); break;
+ case 4: cp = COLOR_PAIR(BLUE); break;
+ case 5: cp = COLOR_PAIR(CYAN); break;
+ case 6: cp = COLOR_PAIR(MAGENTA); break;
+ case 7: cp = COLOR_PAIR(BLACK); break;
+ case 8: cp = COLOR_PAIR(BLUE); break;
+ }
+ if (HIGHLIGHT_SQUARES && cp) cp |= A_REVERSE;
+ if (done == GAME_OVER) cp = 0;
+ attron(cp);
+ addch(s.neighbors > 0 ? '0' + s.neighbors : ' ');
+ attroff(cp);
+ } else {
+ int cp = COLOR_PAIR(GREEN);
+ if (done == GAME_OVER) cp = 0;
+ attron(cp);
+ addch('#');
+ attroff(cp);
+ }
+ addch(' ');
+ }
+ addstr("|\n");
+
+ for (int x = 0; x < X; x++) addstr("+ - ");
+ addstr("+\n");
+ }
+
+ int y, x;
+ getyx(stdscr, y, x);
+ move(y, (X*4+2)/2 - (24+nlen(MINES)+nlen(total_flags))/2);
+ printw("MINES: %d FLAGS PLACED: %d\n", MINES, total_flags);
+ move(y+1, (X*4+2)/2 - 16);
+ printw("'f' to toggle flag mode: now %s\n", flag_mode ? "ON" : "OFF");
+
+ refresh();
+
+ if (display_once_more) break;
+
+ int c = getch();
+ switch (c) {
+ case KEY_MOUSE:;
+ MEVENT event;
+ if (getmouse(&event) != OK) break;
+ // ignore clicks on grid patterning
+ if (event.x % 4 == 0) break;
+ if (event.y % 2 == 0) break;
+ // convert to grid indices
+ int x = (event.x - 2) / 4;
+ int y = (event.y - 1) / 2;
+
+ // fix clicks on left space in each cell
+ if ((event.x - 1) % 4 == 0) x++;
+
+ if (x > X || y > Y) break;
+
+ if (!flag_mode && event.bstate & BUTTON1_CLICKED) {
+ if (!grid_exists) {
+ create_grid(x, y);
+ start_time = time(NULL);
+ grid_exists = true;
+ }
+
+ // clicking on a flag should do nothing
+ if (grid[x][y].flagged) break;
+ if (grid[x][y].mine) {
+ done = GAME_OVER;
+ display_once_more = true;
+ break;
+ }
+
+ // don't waste time if the square is already revealed
+ if (grid[x][y].revealed) break;
+ reveal(x, y);
+
+ if (grid[x][y].neighbors == 0) {
+ reveal_blank(x, y);
+ }
+
+ if (won()) {
+ done = WIN;
+ display_once_more = true;
+ }
+ } else if (flag_mode || (!grid[x][y].revealed && event.bstate & BUTTON3_CLICKED)) {
+ grid[x][y].flagged = !grid[x][y].flagged;
+ if (grid[x][y].flagged) total_flags++;
+ else total_flags--;
+ }
+ break;
+ case 'f':
+ flag_mode = !flag_mode;
+ break;
+ case 'q':
+ case 27:
+ done = QUIT;
+ break;
+ default:
+ break;
+ }
+ }
+
+ switch (done) {
+ case GAME_OVER:
+ move(0, (X*4+2) / 2 - 3);
+ attron(A_REVERSE | COLOR_PAIR(RED));
+ addstr("BOOM!");
+ attroff(A_REVERSE | COLOR_PAIR(RED));
+ break;
+ case WIN:
+ move((Y*2+1)/2, (X*4+2) / 2 - 16);
+ attron(A_REVERSE);
+ addstr("All the mines have been found :)");
+ attroff(A_REVERSE);
+ break;
+ case QUIT:
+ goto terminate;
+ default: break;
+ }
+
+
+ time_t end_time = time(NULL);
+
+ move(Y*2+1, 0);
+ clrtoeol();
+ move(Y*2+1, (X*4+2)/2 - 14);
+ attron(A_REVERSE);
+ addstr("Any to play again, q to quit");
+ attroff(A_REVERSE);
+ move(Y*2+2, 0);
+ clrtoeol();
+ move(Y*2+2, (X*4+2)/2 - (4 + nlen(end_time - start_time)/2));
+ printw("TIME: %ds", end_time - start_time);
+ refresh();
+ if (getch() == 'q') goto terminate;
+
+ global_reset();
+ }
+
+ terminate:
+ echo();
+ curs_set(1);
+ endwin();
+}
diff --git a/snake/README.md b/snake/README.md
new file mode 100644
index 0000000..e7cb09c
--- /dev/null
+++ b/snake/README.md
@@ -00 +111 @@
+# snake
+
+Classic snake game. Move with arrow keys, eat the apples to grow longer.
+
+
+## Config
+
+* `Y`: Height of play area
+* `X`: Width of play area
+* `DELAY`: The delay, in milliseconds, between each movement
+* `WALLS`: Either `true` or `false`. If `true`, enable solid walls (game over on crash). Otherwise, allow snake to teleport through walls.
diff --git a/snake/snake.c b/snake/snake.c
new file mode 100644
index 0000000..56badc0
--- /dev/null
+++ b/snake/snake.c
@@ -00 +1365 @@
+#include <time.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <ncurses.h>
+#include <stdbool.h>
+#include <sys/time.h>
+
+
+/* BEGIN CONFIG */
+#define Y 16
+#define X 32
+
+// time between movements (ms)
+#define DELAY 100
+
+// enable or disable solid walls
+#define WALLS true
+/* END CONFIG */
+
+
+#if WALLS
+#define WALLC '#'
+#else
+#define WALLC ''
+#endif
+
+
+enum rstate {
+ RUNNING,
+ WALL_CRASH,
+ SELF_HIT,
+ QUIT,
+};
+
+
+enum color {
+ RED = 1,
+ MAGENTA,
+ GREEN,
+ CYAN,
+};
+
+
+enum direction {
+ DIR_UP,
+ DIR_DOWN,
+ DIR_LEFT,
+ DIR_RIGHT,
+};
+
+
+struct node {
+ enum direction dir;
+
+ bool pivot;
+ int x;
+ int y;
+
+ struct node * next;
+ struct node * prev;
+};
+
+
+struct snake {
+ struct node * nodes;
+ struct node * last;
+
+ int length;
+};
+
+
+struct apple {
+ int x;
+ int y;
+};
+
+
+int nlen(int n) {
+ int len = 0;
+ while (n > 0) {
+ len++;
+ n /= 10;
+ }
+ return len;
+}
+
+
+int main() {
+ srand(time(NULL));
+
+ initscr();
+ noecho();
+ keypad(stdscr, true);
+ curs_set(0);
+ // the snake should move once every DELAY ms,
+ // no matter if input is recieved
+ timeout(DELAY);
+
+ use_default_colors();
+ start_color();
+ init_pair(RED, COLOR_RED, -1);
+ init_pair(MAGENTA, COLOR_MAGENTA, -1);
+ init_pair(GREEN, COLOR_GREEN, -1);
+ init_pair(CYAN, COLOR_CYAN, -1);
+
+ while (true) {
+ struct snake s = {
+ .nodes = &(struct node){
+ .dir = DIR_RIGHT,
+ .pivot = false,
+ .x = X / 2,
+ .y = Y / 2,
+ .next = NULL,
+ .prev = NULL,
+ },
+ .last = NULL,
+ .length = 1,
+ };
+ s.last = s.nodes;
+
+ struct apple a = {
+ .x = rand() % X,
+ .y = rand() % Y,
+ };
+
+ int score = 1;
+
+ enum rstate state = RUNNING;
+ while (true) {
+ // ate an apple!
+ if (s.nodes->x == a.x && s.nodes->y == a.y) {
+ score++;
+
+ struct node * l = s.last;
+ // add a new node
+ s.length++;
+ l->next = malloc(sizeof(struct node));
+ struct node * nw = l->next;
+ nw->dir = l->dir;
+ nw->pivot = false;
+
+ switch (l->dir) {
+ case DIR_UP:
+ nw->x = l->x;
+ nw->y = l->y + 1;
+ break;
+ case DIR_DOWN:
+ nw->x = l->x;
+ nw->y = l->y - 1;
+ break;
+ case DIR_LEFT:
+ nw->x = l->x + 1;
+ nw->y = l->y;
+ break;
+ case DIR_RIGHT:
+ nw->x = l->x - 1;
+ nw->y = l->y;
+ break;
+ }
+
+ nw->prev = l;
+ nw->next = NULL;
+
+ s.last = l->next;
+
+ a.x = rand() % X;
+ a.y = rand() % Y;
+ }
+
+ // move the snake
+ for (struct node * n = s.last; n; n = n->prev) {
+ switch (n->dir) {
+ case DIR_UP:
+ n->y--;
+ break;
+ case DIR_DOWN:
+ n->y++;
+ break;
+ case DIR_LEFT:
+ n->x--;
+ break;
+ case DIR_RIGHT:
+ n->x++;
+ break;
+ }
+
+ // teleport the snake in no-walls mode
+ if (MODE == NOWALLS) {
+ if (n->x < 0) n->x = X - 1;
+ else if (n->x >= X) n->x = 0;
+
+ if (n->y < 0) n->y = Y - 1;
+ else if (n->y >= Y) n->y = 0;
+ }
+
+ // set the direction of the next node to that of the previous one
+ if (n->prev && n->prev->pivot) {
+ n->dir = n->prev->dir;
+ n->prev->pivot = false;
+ n->pivot = true;
+ }
+ }
+
+ // handle wall crash
+ if (MODE == WALLS && (s.nodes->x < 0 || s.nodes->x >= X || s.nodes->y < 0 || s.nodes->y >= Y)) {
+ state = WALL_CRASH;
+ goto done;
+ }
+
+ // handle bumping into self
+ for (struct node * n = s.nodes->next; n; n = n->next) {
+ if (s.nodes->x == n->x && s.nodes->y == n->y) {
+ state = SELF_HIT;
+ goto done;
+ }
+ }
+
+ erase();
+ // draw the border
+ attron(A_REVERSE | A_DIM | COLOR_PAIR(MAGENTA));
+ for (int i = 0; i <= X + 1; i++) { // top
+ move(0, i);
+ addch(WALLC);
+ }
+ for (int i = 1; i < Y + 1; i++) { // middle
+ move(i, 0);
+ addch(WALLC);
+ move(i, X + 1);
+ addch(WALLC);
+
+ }
+ for (int i = 0; i <= X + 1; i++) { // bottom
+ attron(A_REVERSE);
+ move(Y + 1, i);
+ addch(WALLC);
+ }
+ attroff(A_REVERSE | A_DIM | COLOR_PAIR(MAGENTA));
+ // write the score
+ move(0, X / 2 - 3 - nlen(score) / 2);
+ attron(A_UNDERLINE);
+ printw("Score: %d", score);
+ attroff(A_UNDERLINE);
+ // draw the snake
+ move(s.nodes->y + 1, s.nodes->x + 1);
+ // first the head
+ attron(COLOR_PAIR(CYAN));
+ switch (s.nodes->dir) {
+ case DIR_UP:
+ addch('^');
+ break;
+ case DIR_DOWN:
+ addch('v');
+ break;
+ case DIR_LEFT:
+ addch('<');
+ break;
+ case DIR_RIGHT:
+ addch('>');
+ break;
+ }
+ attroff(COLOR_PAIR(CYAN));
+ // then the body
+ attron(COLOR_PAIR(GREEN));
+ for (struct node * n = s.nodes->next; n; n = n->next) {
+ move(n->y + 1, n->x + 1);
+ addch('*');
+ }
+ attroff(COLOR_PAIR(GREEN));
+ // draw the apple
+ move(a.y + 1, a.x + 1);
+ attron(COLOR_PAIR(RED));
+ addch('@');
+ attroff(COLOR_PAIR(RED));
+ refresh();
+
+ // handle input
+ struct timeval start;
+ gettimeofday(&start, NULL);
+
+ int c = getch();
+ switch (c) {
+ case KEY_UP:
+ if (s.length > 1 && s.nodes->dir == DIR_DOWN) break;
+ s.nodes->dir = DIR_UP;
+ s.nodes->pivot = true;
+ break;
+ case KEY_DOWN:
+ if (s.length > 1 && s.nodes->dir == DIR_UP) break;
+ s.nodes->dir = DIR_DOWN;
+ s.nodes->pivot = true;
+ break;
+ break;
+ case KEY_LEFT:
+ if (s.length > 1 && s.nodes->dir == DIR_RIGHT) break;
+ s.nodes->dir = DIR_LEFT;
+ s.nodes->pivot = true;
+ break;
+ case KEY_RIGHT:
+ if (s.length > 1 && s.nodes->dir == DIR_LEFT) break;
+ s.nodes->dir = DIR_RIGHT;
+ s.nodes->pivot = true;
+ break;
+ case ' ':
+ timeout(-1);
+ move(Y / 2, X / 2 - 2);
+ attron(A_REVERSE);
+ addstr("Paused");
+ attroff(A_REVERSE);
+ int c;
+ while ((c = getch()) != ' ') {
+ if (c == 'q') {
+ state = QUIT;
+ goto done;
+ }
+ }
+ timeout(DELAY);
+ break;
+ case 'q':
+ state = QUIT;
+ goto done;
+ }
+
+ struct timeval end;
+ gettimeofday(&end, NULL);
+
+ unsigned delta_ms = (end.tv_usec - start.tv_usec) * 1000;
+ if (delta_ms < DELAY) {
+ napms(delta_ms);
+ }
+ }
+
+ done:
+ switch (state) {
+ case QUIT:
+ goto terminate;
+ case WALL_CRASH:
+ move(Y / 2, X / 2 - 12);
+ attron(A_REVERSE);
+ addstr("You crashed into the wall");
+ attroff(A_REVERSE);
+ refresh();
+ napms(1500);
+ break;
+ case SELF_HIT:
+ move(Y / 2, X / 2 - 10);
+ attron(A_REVERSE);
+ addstr("You ran into yourself");
+ attroff(A_REVERSE);
+ refresh();
+ napms(1500);
+ break;
+ default: break;
+ }
+
+ for (struct node * n = s.nodes->next; n; n = n->next) {
+ free(n);
+ }
+ }
+
+ terminate:
+ curs_set(1);
+ keypad(stdscr, false);
+ echo();
+ endwin();
+}