QUOTE: Never too old to chase dreams.

cmine

A tiny terminal-based Minesweeper

commit 7a164b1a54368a57b92b5e920bcb2f74db2d9c5f
Author: Sophie <info@soophie.de>
Date:   Mon, 12 May 2025 22:53:30 +0200

feat: Initial commit

Diffstat:
AMakefile | 10++++++++++
Asrc/main.c | 482+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 492 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,10 @@ +CC=cc +CLFAGS=-Wall -Wextra -Werror -pendatic +LIBS=-lm +TARGET=cmine + +build: src/*.c + $(CC) $(CFLAGS) -o $(TARGET) $^ $(LIBS) + +clean: + rm ./$(TARGET) diff --git a/src/main.c b/src/main.c @@ -0,0 +1,482 @@ +#include <stdlib.h> +#include <getopt.h> + +#define LIB_TERM_IMPL +#include <libterm.h> + +#define MASK_DIGIT 0x0f + +#define FLAG_OPEN 0x10 +#define FLAG_BOMB 0x20 +#define FLAG_FLAG 0x40 +#define FLAG_DIRTY 0x80 + +#define BOARD_SIZE 2500 + +typedef unsigned char u8_t; +typedef unsigned int u32_t; + +typedef enum { + STATE_PLAYING, + STATE_DEFEAT, + STATE_VICTORY, +} state_t; + +typedef struct { + int y; + int x; +} vec2_t; + +typedef struct { + char name[100]; + int rows; + int cols; + int bombs; +} level_t; + +typedef struct { + state_t state; + level_t level; + u8_t board[BOARD_SIZE]; + vec2_t size; + vec2_t window; + vec2_t cursor; + int seed; + int bombs; + int count; + time_t time; +} cmine_t; + +static struct option OPTIONS_LEVEL[] = { + { "easy", no_argument, 0, 'e' }, + { "medium", no_argument, 0, 'm' }, + { "hard", no_argument, 0, 'h' }, + { "seed", required_argument, 0, 's' }, + { 0, 0, 0, 0 }, +}; + +static level_t LEVEL_EASY = { "EASY", 9, 9, 10 }; +static level_t LEVEL_MEDIUM = { "MEDIUM", 16, 16, 40 }; +static level_t LEVEL_HARD = { "HARD", 16, 30, 99 }; + +static const u32_t COLOR_NONE = 0x000000; +static const u32_t COLOR_CURSOR = 0xffffff; +static const u32_t COLOR_COVER = 0xcccccc; +static const u32_t COLOR_BOMB = 0xff0000; +static const u32_t COLOR_FLAG = 0xffff00; +static const u32_t COLOR_DIGITS[9] = { + 0x777777, + 0x6666ff, + 0x66a066, + 0xff6666, + 0xaa66ff, + 0xffaa33, + 0x00aaaa, + 0xaaaa00, + 0xaaaaaa, +}; + +int is_valid(cmine_t *self, int y, int x) { + return y >= 0 && y < self->size.y && x >= 0 && x < self->size.x; +} + +int is_flag(u8_t cell, int flag) { + return (cell & flag) != 0; +} + +u8_t *cell_at(cmine_t *self, int y, int x) { + return &self->board[y * self->size.x + x]; +} + +void victory(cmine_t *self) { + self->state = STATE_VICTORY; + // flag all bombs + for (int y = 0; y < self->size.y; y++) { + for (int x = 0; x < self->size.x; x++) { + u8_t *cell = cell_at(self, y, x); + if (!is_flag(*cell, FLAG_BOMB)) { + continue; + } + if (is_flag(*cell, FLAG_FLAG)) { + continue; + } + *cell |= FLAG_FLAG; + *cell |= FLAG_DIRTY; + } + } +} + +void defeat(cmine_t *self) { + self->state = STATE_DEFEAT; + // reveal all bombs + for (int y = 0; y < self->size.y; y++) { + for (int x = 0; x < self->size.x; x++) { + u8_t *cell = cell_at(self, y, x); + if (!is_flag(*cell, FLAG_BOMB)) { + continue; + } + if (is_flag(*cell, FLAG_OPEN)) { + continue; + } + *cell |= FLAG_OPEN; + *cell |= FLAG_DIRTY; + } + } +} + +void open(cmine_t *self, int y, int x) { + if (self->state != STATE_PLAYING) { + return; + } + u8_t *cell = cell_at(self, y, x); + // ignore flags + if (is_flag(*cell, FLAG_FLAG)) { + return; + } + // handle open cells + // chord opening + if (is_flag(*cell, FLAG_OPEN)) { + int digit = *cell & MASK_DIGIT; + if (digit == 0) { + return; + } + int flags = 0; + for (int ly = -1; ly <= 1; ly++) { + for (int lx = -1; lx <= 1; lx++) { + if (!is_valid(self, y + ly, x + lx)) { + continue; + } + if (ly == 0 && lx == 0) { + continue; + } + u8_t *l_cell = cell_at(self, y + ly, x + lx); + if (is_flag(*l_cell, FLAG_FLAG)) { + flags++; + } + } + } + if (flags == digit) { + for (int ly = -1; ly <= 1; ly++) { + for (int lx = -1; lx <= 1; lx++) { + if (!is_valid(self, y + ly, x + lx)) { + continue; + } + if (ly == 0 && lx == 0) { + continue; + } + u8_t *l_cell = cell_at(self, y + ly, x + lx); + if (!is_flag(*l_cell, FLAG_OPEN)) { + open(self, y + ly, x + lx); + } + } + } + } + } + // handle covered cells + else { + *cell |= FLAG_OPEN; + *cell |= FLAG_DIRTY; + // check if bomb + if (is_flag(*cell, FLAG_BOMB)) { + defeat(self); + return; + } + int digit = *cell & MASK_DIGIT; + // uncover surrounding cells + if (digit == 0) { + for (int ly = -1; ly <= 1; ly++) { + for (int lx = -1; lx <= 1; lx++) { + if (!is_valid(self, y + ly, x + lx)) { + continue; + } + if (ly == 0 && lx == 0) { + continue; + } + open(self, y + ly, x + lx); + } + } + } + } + // check if non-victory + for (int y = 0; y < self->size.y; y++) { + for (int x = 0; x < self->size.x; x++) { + u8_t *l_cell = cell_at(self, y, x); + if (!is_flag(*l_cell, FLAG_BOMB) && !is_flag(*l_cell, FLAG_OPEN)) { + return; + } + } + } + victory(self); +} + +void flag(cmine_t *self, int y, int x) { + if (self->state != STATE_PLAYING) { + return; + } + u8_t *cell = cell_at(self, y, x); + // ignore open + if (is_flag(*cell, FLAG_OPEN)) { + return; + } + *cell |= FLAG_DIRTY; + // unset flag + if (is_flag(*cell, FLAG_FLAG)) { + *cell &= ~FLAG_FLAG; + self->count++; + } + // set flag + else { + *cell |= FLAG_FLAG; + self->count--; + } +} + +void move(cmine_t *self, int dy, int dx) { + if (self->state != STATE_PLAYING) { + return; + } + if (!is_valid(self, self->cursor.y + dy, self->cursor.x + dx)) { + return; + } + *cell_at(self, self->cursor.y, self->cursor.x) |= FLAG_DIRTY; + self->cursor = (vec2_t) { self->cursor.y + dy, self->cursor.x + dx }; + *cell_at(self, self->cursor.y, self->cursor.x) |= FLAG_DIRTY; +} + +void generate(cmine_t *self, level_t level, int seed) { + // init self + (*self) = (cmine_t) { + .state = STATE_PLAYING, + .level = level, + .board = {0}, + .size = { level.rows, level.cols }, + .window = { 0, 0 }, + .cursor = { 0, 0 }, + .seed = seed, + .bombs = level.bombs, + .count = level.bombs, + .time = -1, + }; + srand(self->seed); + // flag all dirty + memset(self->board, 0, BOARD_SIZE); + for (int y = 0; y < self->size.y; y++) { + for (int x = 0; x < self->size.x; x++) { + *cell_at(self, y, x) = FLAG_DIRTY; + } + } + // place bombs + int bombs = self->bombs; + while (bombs > 0) { + int y = rand() % self->size.y; + int x = rand() % self->size.x; + u8_t *cell = cell_at(self, y, x); + if (!is_flag(*cell, FLAG_BOMB)) { + *cell |= FLAG_BOMB; + bombs--; + } + } + // place numbers + for (int y = 0; y < self->size.y; y++) { + for (int x = 0; x < self->size.x; x++) { + u8_t *cell = cell_at(self, y, x); + for (int ly = -1; ly <= 1; ly++) { + for (int lx = -1; lx <= 1; lx++) { + if (!is_valid(self, y + ly, x + lx)) { + continue; + } + if (ly == 0 && lx == 0) { + continue; + } + u8_t *l_cell = cell_at(self, y + ly, x + lx); + if (is_flag(*l_cell, FLAG_BOMB)) { + (*cell)++; + } + } + } + } + } + // find starting position + int count = 0; + vec2_t sel = { -1, -1 }; + for (int y = 0; y < self->size.y; y++) { + for (int x = 0; x < self->size.x; x++) { + u8_t *cell = cell_at(self, y, x); + int digit = *cell & MASK_DIGIT; + if (is_flag(*cell, FLAG_BOMB) || digit != 0) { + continue; + } + count++; + if (rand() % count != 0) { + continue; + } + sel = (vec2_t) { y, x }; + } + } + if (sel.y == -1 || sel.x == -1) { + self->cursor = (vec2_t) { 0, 0 }; + return; + } + self->cursor = sel; + // uncover starting position + open(self, self->cursor.y, self->cursor.x); +} + +void update(term_t *term, cmine_t *self) { + // check window resize + size_t new_height; + size_t new_width; + term_read_window(term, &new_height, &new_width); + int is_resize = new_height != self->window.y || new_width != self->window.x; + term_write(term, "\x1b[?25l"); + if (is_resize) { + self->window = (vec2_t) { new_height, new_width }; + } + int set_y = self->window.y / 2 - self->size.y / 2; + int set_x = self->window.x / 2 - (2 * self->size.x) / 2; + if (is_resize) { + term_write(term, "\x1b[2J"); + } + // render info + if (self->state == STATE_PLAYING) { + time_t now = time(NULL); + time_t duration = difftime(now, self->time != -1 ? self->time : now); + int minutes = (duration % 3600) / 60; + int seconds = duration % 60; + term_writef(term, "\x1b[%d;%dH", set_y - 2 + 1, set_x + 1); + term_writef(term, "\x1b[38;2;255;255;255m%02d:%02d", minutes, seconds); + term_writef(term, " | %dx%d\x1b[39m", self->level.cols, self->level.rows); + term_writef(term, " \x1b[38;2;255;255;0m(%d)\x1b[39m\x1b[0K", self->count); + term_flush(term); + } + // render board + for (int y = 0; y < self->size.y; y++) { + for (int x = 0; x < self->size.x; x++) { + u8_t *cell = cell_at(self, y, x); + // ignore non-dirty cells + if (!is_resize && !is_flag(*cell, FLAG_DIRTY)) { + continue; + } + *cell &= ~ FLAG_DIRTY; + // determine color and symbol + int is_bomb = is_flag(*cell, FLAG_BOMB); + int is_open = is_flag(*cell, FLAG_OPEN); + int digit = *cell & MASK_DIGIT; + char symbol = '?'; + u32_t color = COLOR_COVER; + if (!is_open && is_flag(*cell, FLAG_FLAG)) { + symbol = 'F'; + color = COLOR_FLAG; + } + if (is_open && is_bomb) { + symbol = '*'; + color = COLOR_BOMB; + } + if (is_open && !is_bomb) { + symbol = '0' + digit; + if (digit == 0) { + symbol = '.'; + } + color = COLOR_DIGITS[digit]; + } + if (self->cursor.y == y && self->cursor.x == x) { + color = COLOR_CURSOR; + } + // render cell + term_writef(term, "\x1b[%d;%dH", set_y + y + 1, set_x + (2 * x) + 1); + int red = (color >> 16) & 0xff; + int green = (color >> 8) & 0xff; + int blue = (color >> 0) & 0xff; + term_writef(term, "\x1b[38;2;%d;%d;%dm", red, green, blue); + term_writef(term, "%c", symbol); + term_write(term, "\x1b[39m"); + } + } + term_write(term, "\x1b[?25h"); + // move cursor + int cy = set_y + self->cursor.y + 1; + int cx = set_x + (2 * self->cursor.x) + 1; + term_writef(term, "\x1b[%d;%dH", cy, cx); + term_flush(term); +} + +int main(int argc, char *argv[]) { + // parse arguments + srand(time(NULL)); + int seed = rand(); + level_t *level = NULL; + opterr = 0; + int opt; + while ((opt = getopt_long(argc, argv, "emhs:", OPTIONS_LEVEL, NULL)) != -1) { + switch (opt) { + case 'e': + level = &LEVEL_EASY; + break; + case 'm': + level = &LEVEL_MEDIUM; + break; + case 'h': + level = &LEVEL_HARD; + break; + case 's': + seed = atoi(optarg); + break; + default: + fprintf(stderr, "Usage: %s [--easy|--medium|--hard]\n", argv[0]); + exit(EXIT_FAILURE); + } + } + if (level == NULL) { + fprintf(stderr, "Usage: %s [--easy|--medium|--hard]\n", argv[0]); + exit(EXIT_FAILURE); + } + // update loop + term_t term = term_init(); + term_rawmode_enable(&term); + cmine_t self; + generate(&self, *level, seed); + term_write(&term, "\x1b[?1049h"); + term_flush(&term); + int quit = 0; + while (!quit) { + update(&term, &self); + int key = term_poll_key(&term, 100); + switch (key) { + case 'q': + quit = 1; + break; + case KEY_ESC: + generate(&self, *level, ++seed); + break; + case 'j': + move(&self, 1, 0); + break; + case 'k': + move(&self, -1, 0); + break; + case 'l': + move(&self, 0, 1); + break; + case 'h': + move(&self, 0, -1); + break; + case ' ': + // start timer + if (self.time == -1) { + self.time = time(NULL); + } + open(&self, self.cursor.y, self.cursor.x); + break; + case 'f': + flag(&self, self.cursor.y, self.cursor.x); + break; + default: + break; + } + } + term_write(&term, "\x1b[?1049l"); + term_flush(&term); + term_rawmode_disable(&term); + term_cleanup(&term); + return EXIT_SUCCESS; +}