Thumbnail

rani/games.git

Clone URL: https://git.buni.party/rani/games.git

commit 53bf7297fc90a266c23c6ff6054efb06d2cd1478 Author: rani <clagv.randomgames@gmail.com> Date: Thu Jul 20 14:25:10 2023 +0000 Initial commit 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(); +}