commit 7a164b1a54368a57b92b5e920bcb2f74db2d9c5f
Author: Sophie <info@soophie.de>
Date: Mon, 12 May 2025 22:53:30 +0200
feat: Initial commit
Diffstat:
A | Makefile | | | 10 | ++++++++++ |
A | src/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;
+}