QUOTE: Be someone’s rainbow today.

cmine

A tiny terminal-based Minesweeper

main.c (12052B)


      1#include <stdlib.h>
      2#include <getopt.h>
      3#include <time.h>
      4
      5#define LIB_TERM_IMPL
      6#include <libterm.h>
      7
      8#define MASK_DIGIT 0x0f
      9
     10#define FLAG_OPEN  0x10
     11#define FLAG_BOMB  0x20
     12#define FLAG_FLAG  0x40
     13#define FLAG_DIRTY 0x80
     14
     15#define BOARD_SIZE  2500
     16
     17typedef unsigned char u8_t;
     18typedef unsigned int u32_t;
     19
     20typedef enum {
     21  STATE_PLAYING,
     22  STATE_DEFEAT,
     23  STATE_VICTORY,
     24} state_t;
     25
     26typedef struct {
     27  int y;
     28  int x;
     29} vec2_t;
     30
     31typedef struct {
     32  char name[100];
     33  int rows;
     34  int cols;
     35  int bombs;
     36} level_t;
     37
     38typedef struct {
     39  state_t state;
     40  level_t level;
     41  u8_t board[BOARD_SIZE];
     42  vec2_t size;
     43  vec2_t window;
     44  vec2_t cursor;
     45  int seed;
     46  int bombs;
     47  int count;
     48  time_t time;
     49} cmine_t;
     50
     51static struct option OPTIONS_LEVEL[] = {
     52  { "easy",   no_argument,       0, 'e' },
     53  { "medium", no_argument,       0, 'm' },
     54  { "hard",   no_argument,       0, 'h' },
     55  { "seed",   required_argument, 0, 's' },
     56  { 0, 0, 0, 0 },
     57};
     58
     59static level_t LEVEL_EASY   = { "EASY",    9,  9, 10 };
     60static level_t LEVEL_MEDIUM = { "MEDIUM", 16, 16, 40 };
     61static level_t LEVEL_HARD   = { "HARD",   16, 30, 99 };
     62
     63static const u32_t COLOR_CURSOR = 0xffffff;
     64static const u32_t COLOR_COVER  = 0xcccccc;
     65static const u32_t COLOR_BOMB   = 0xff0000;
     66static const u32_t COLOR_FLAG   = 0xffff00;
     67static const u32_t COLOR_DIGITS[9] = {
     68  0x777777,
     69  0x6666ff,
     70  0x66a066,
     71  0xff6666,
     72  0xaa66ff,
     73  0xffaa33,
     74  0x00aaaa,
     75  0xaaaa00,
     76  0xaaaaaa,
     77};
     78
     79int cm_is_valid(cmine_t *self, int y, int x) {
     80  return y >= 0 && y < self->size.y && x >= 0 && x < self->size.x;
     81}
     82
     83int cm_is_flag(u8_t cell, int flag) {
     84  return (cell & flag) != 0;
     85}
     86
     87u8_t *cell_at(cmine_t *self, int y, int x) {
     88  return &self->board[y * self->size.x + x];
     89}
     90
     91void cm_victory(cmine_t *self) {
     92  self->state = STATE_VICTORY;
     93  // flag all bombs
     94  for (int y = 0; y < self->size.y; y++) {
     95    for (int x = 0; x < self->size.x; x++) {
     96      u8_t *cell = cell_at(self, y, x);
     97      if (!cm_is_flag(*cell, FLAG_BOMB)) {
     98        continue;
     99      }
    100      if (cm_is_flag(*cell, FLAG_FLAG)) {
    101        continue;
    102      }
    103      *cell |= FLAG_FLAG;
    104      *cell |= FLAG_DIRTY;
    105    }
    106  }
    107}
    108
    109void cm_defeat(cmine_t *self) {
    110  self->state = STATE_DEFEAT;
    111  // reveal all bombs
    112  for (int y = 0; y < self->size.y; y++) {
    113    for (int x = 0; x < self->size.x; x++) {
    114      u8_t *cell = cell_at(self, y, x);
    115      if (!cm_is_flag(*cell, FLAG_BOMB)) {
    116        continue;
    117      }
    118      if (cm_is_flag(*cell, FLAG_OPEN)) {
    119        continue;
    120      }
    121      *cell |= FLAG_OPEN;
    122      *cell |= FLAG_DIRTY;
    123    }
    124  }
    125}
    126
    127void cm_open(cmine_t *self, int y, int x) {
    128  if (self->state != STATE_PLAYING) {
    129    return;
    130  }
    131  u8_t *cell = cell_at(self, y, x);
    132  // ignore flags
    133  if (cm_is_flag(*cell, FLAG_FLAG)) {
    134    return;
    135  }
    136  // handle open cells
    137  // chord opening
    138  if (cm_is_flag(*cell, FLAG_OPEN)) {
    139    int digit = *cell & MASK_DIGIT;
    140    if (digit == 0) {
    141      return;
    142    }
    143    int flags = 0;
    144    for (int ly = -1; ly <= 1; ly++) {
    145      for (int lx = -1; lx <= 1; lx++) {
    146        if (!cm_is_valid(self, y + ly, x + lx)) {
    147          continue;
    148        }
    149        if (ly == 0 && lx == 0) {
    150          continue;
    151        }
    152        u8_t *l_cell = cell_at(self, y + ly, x + lx);
    153        if (cm_is_flag(*l_cell, FLAG_FLAG)) {
    154          flags++;
    155        }
    156      }
    157    }
    158    if (flags == digit) {
    159      for (int ly = -1; ly <= 1; ly++) {
    160        for (int lx = -1; lx <= 1; lx++) {
    161          if (!cm_is_valid(self, y + ly, x + lx)) {
    162            continue;
    163          }
    164          if (ly == 0 && lx == 0) {
    165            continue;
    166          }
    167          u8_t *l_cell = cell_at(self, y + ly, x + lx);
    168          if (!cm_is_flag(*l_cell, FLAG_OPEN)) {
    169            cm_open(self, y + ly, x + lx);
    170          }
    171        }
    172      }
    173    }
    174  }
    175  // handle covered cells
    176  else {
    177    *cell |= FLAG_OPEN;
    178    *cell |= FLAG_DIRTY;
    179    // check if bomb
    180    if (cm_is_flag(*cell, FLAG_BOMB)) {
    181      cm_defeat(self);
    182      return;
    183    }
    184    int digit = *cell & MASK_DIGIT;
    185    // uncover surrounding cells
    186    if (digit == 0) {
    187      for (int ly = -1; ly <= 1; ly++) {
    188        for (int lx = -1; lx <= 1; lx++) {
    189          if (!cm_is_valid(self, y + ly, x + lx)) {
    190            continue;
    191          }
    192          if (ly == 0 && lx == 0) {
    193            continue;
    194          }
    195          cm_open(self, y + ly, x + lx);
    196        }
    197      }
    198    }
    199  }
    200  // check if non-victory
    201  for (int y = 0; y < self->size.y; y++) {
    202    for (int x = 0; x < self->size.x; x++) {
    203      u8_t *l_cell = cell_at(self, y, x);
    204      if (!cm_is_flag(*l_cell, FLAG_BOMB) && !cm_is_flag(*l_cell, FLAG_OPEN)) {
    205        return;
    206      }
    207    }
    208  }
    209  cm_victory(self);
    210}
    211
    212void cm_flag(cmine_t *self, int y, int x) {
    213  if (self->state != STATE_PLAYING) {
    214    return;
    215  }
    216  u8_t *cell = cell_at(self, y, x);
    217  // ignore open
    218  if (cm_is_flag(*cell, FLAG_OPEN)) {
    219    return;
    220  }
    221  *cell |= FLAG_DIRTY;
    222  // unset flag
    223  if (cm_is_flag(*cell, FLAG_FLAG)) {
    224    *cell &= ~FLAG_FLAG;
    225    self->count++;
    226  }
    227  // set flag
    228  else {
    229    *cell |= FLAG_FLAG;
    230    self->count--;
    231  }
    232}
    233
    234void cm_move(cmine_t *self, int dy, int dx) {
    235  if (self->state != STATE_PLAYING) {
    236    return;
    237  }
    238  if (!cm_is_valid(self, self->cursor.y + dy, self->cursor.x + dx)) {
    239    return;
    240  }
    241  *cell_at(self, self->cursor.y, self->cursor.x) |= FLAG_DIRTY;
    242  self->cursor = (vec2_t) { self->cursor.y + dy, self->cursor.x + dx };
    243  *cell_at(self, self->cursor.y, self->cursor.x) |= FLAG_DIRTY;
    244}
    245
    246void cm_generate(cmine_t *self, level_t level, int seed) {
    247  // init self
    248  (*self) = (cmine_t) {
    249    .state = STATE_PLAYING,
    250    .level = level,
    251    .board = {0},
    252    .size = { level.rows, level.cols },
    253    .window = { 0, 0 },
    254    .cursor = { 0, 0 },
    255    .seed = seed,
    256    .bombs = level.bombs,
    257    .count = level.bombs,
    258    .time = -1,
    259  };
    260  srand(self->seed);
    261  // flag all dirty
    262  memset(self->board, 0, BOARD_SIZE);
    263  for (int y = 0; y < self->size.y; y++) {
    264    for (int x = 0; x < self->size.x; x++) {
    265      *cell_at(self, y, x) = FLAG_DIRTY;
    266    }
    267  }
    268  // place bombs
    269  int bombs = self->bombs;
    270  while (bombs > 0) {
    271    int y = rand() % self->size.y;
    272    int x = rand() % self->size.x;
    273    u8_t *cell = cell_at(self, y, x);
    274    if (!cm_is_flag(*cell, FLAG_BOMB)) {
    275      *cell |= FLAG_BOMB;
    276      bombs--;
    277    }
    278  }
    279  // place numbers
    280  for (int y = 0; y < self->size.y; y++) {
    281    for (int x = 0; x < self->size.x; x++) {
    282      u8_t *cell = cell_at(self, y, x);
    283      for (int ly = -1; ly <= 1; ly++) {
    284        for (int lx = -1; lx <= 1; lx++) {
    285          if (!cm_is_valid(self, y + ly, x + lx)) {
    286            continue;
    287          }
    288          if (ly == 0 && lx == 0) {
    289            continue;
    290          }
    291          u8_t *l_cell = cell_at(self, y + ly, x + lx);
    292          if (cm_is_flag(*l_cell, FLAG_BOMB)) {
    293            (*cell)++;
    294          }
    295        }
    296      }
    297    }
    298  }
    299  // find starting position
    300  int count = 0;
    301  vec2_t sel = { -1, -1 };
    302  for (int y = 0; y < self->size.y; y++) {
    303    for (int x = 0; x < self->size.x; x++) {
    304      u8_t *cell = cell_at(self, y, x);
    305      int digit = *cell & MASK_DIGIT;
    306      if (cm_is_flag(*cell, FLAG_BOMB) || digit != 0) {
    307        continue;
    308      }
    309      count++;
    310      if (rand() % count != 0) {
    311        continue;
    312      }
    313      sel = (vec2_t) { y, x };
    314    }
    315  }
    316  if (sel.y == -1 || sel.x == -1) {
    317    self->cursor = (vec2_t) { 0, 0 };
    318    return;
    319  }
    320  self->cursor = sel;
    321  // uncover starting position
    322  cm_open(self, self->cursor.y, self->cursor.x);
    323}
    324
    325void cm_update(term_t *term, cmine_t *self) {
    326  // check window resize
    327  size_t new_height;
    328  size_t new_width;
    329  term_read_window(term, &new_height, &new_width);
    330  int is_resize = (int) new_height != self->window.y || (int) new_width != self->window.x;
    331  term_write(term, "\x1b[?25l");
    332  if (is_resize) {
    333    self->window = (vec2_t) { new_height, new_width };
    334  }
    335  int set_y = self->window.y / 2 - self->size.y / 2;
    336  int set_x = self->window.x / 2 - (2 * self->size.x) / 2;
    337  if (is_resize) {
    338    term_write(term, "\x1b[2J");
    339  }
    340  // render info
    341  if (self->state == STATE_PLAYING) {
    342    time_t now = time(NULL);
    343    time_t duration = difftime(now, self->time != -1 ? self->time : now);
    344    int minutes = (duration % 3600) / 60;
    345    int seconds = duration % 60;
    346    term_writef(term, "\x1b[%d;%dH", set_y - 2 + 1, set_x + 1);
    347    term_writef(term, "\x1b[38;2;255;255;255m%02d:%02d", minutes, seconds);
    348    term_writef(term, " | %dx%d\x1b[39m", self->level.cols, self->level.rows);
    349    term_writef(term, " \x1b[38;2;255;255;0m(%d)\x1b[39m\x1b[0K", self->count);
    350    term_flush(term);
    351  }
    352  // render board
    353  for (int y = 0; y < self->size.y; y++) {
    354    for (int x = 0; x < self->size.x; x++) {
    355      u8_t *cell = cell_at(self, y, x);
    356      // ignore non-dirty cells
    357      if (!is_resize && !cm_is_flag(*cell, FLAG_DIRTY)) {
    358        continue;
    359      }
    360      *cell &= ~ FLAG_DIRTY;
    361      // determine color and symbol
    362      int is_bomb = cm_is_flag(*cell, FLAG_BOMB);
    363      int is_open = cm_is_flag(*cell, FLAG_OPEN);
    364      int digit = *cell & MASK_DIGIT;
    365      char symbol = '?';
    366      u32_t color = COLOR_COVER;
    367      if (!is_open && cm_is_flag(*cell, FLAG_FLAG)) {
    368        symbol = 'F';
    369        color = COLOR_FLAG;
    370      }
    371      if (is_open && is_bomb) {
    372        symbol = '*';
    373        color = COLOR_BOMB;
    374      }
    375      if (is_open && !is_bomb) {
    376        symbol = '0' + digit;
    377        if (digit == 0) {
    378          symbol = '.';
    379        }
    380        color = COLOR_DIGITS[digit];
    381      }
    382      if (self->cursor.y == y && self->cursor.x == x) {
    383        color = COLOR_CURSOR;
    384      }
    385      // render cell
    386      term_writef(term, "\x1b[%d;%dH", set_y + y + 1, set_x + (2 * x) + 1);
    387      int red   = (color >> 16) & 0xff;
    388      int green = (color >>  8) & 0xff;
    389      int blue  = (color >>  0) & 0xff;
    390      term_writef(term, "\x1b[38;2;%d;%d;%dm", red, green, blue);
    391      term_writef(term, "%c", symbol);
    392      term_write(term, "\x1b[39m");
    393    }
    394  }
    395  term_write(term, "\x1b[?25h");
    396  // move cursor
    397  int cy = set_y + self->cursor.y + 1;
    398  int cx = set_x + (2 * self->cursor.x) + 1;
    399  term_writef(term, "\x1b[%d;%dH", cy, cx);
    400  term_flush(term);
    401}
    402
    403int main(int argc, char *argv[]) {
    404  // parse arguments
    405  srand(time(NULL));
    406  int seed = rand();
    407  level_t *level = NULL;
    408  opterr = 0;
    409  int opt;
    410  while ((opt = getopt_long(argc, argv, "emhs:", OPTIONS_LEVEL, NULL)) != -1) {
    411    switch (opt) {
    412      case 'e':
    413        level = &LEVEL_EASY;
    414        break;
    415      case 'm':
    416        level = &LEVEL_MEDIUM;
    417        break;
    418      case 'h':
    419        level = &LEVEL_HARD;
    420        break;
    421      case 's':
    422        seed = atoi(optarg);
    423        break;
    424      default:
    425        fprintf(stderr, "Usage: %s [--easy|--medium|--hard]\n", argv[0]);
    426        exit(EXIT_FAILURE);
    427    }
    428  }
    429  if (level == NULL) {
    430    fprintf(stderr, "Usage: %s [--easy|--medium|--hard]\n", argv[0]);
    431    exit(EXIT_FAILURE);
    432  }
    433  // update loop
    434  term_t term = term_init();
    435  term_rawmode_enable(&term);
    436  cmine_t self;
    437  cm_generate(&self, *level, seed);
    438  term_write(&term, "\x1b[?1049h");
    439  term_flush(&term);
    440  int quit = 0;
    441  while (!quit) {
    442    cm_update(&term, &self);
    443    int key = term_poll_key(&term, 100);
    444    switch (key) {
    445      case 'q':
    446        quit = 1;
    447        break;
    448      case KEY_ESC:
    449        cm_generate(&self, *level, ++seed);
    450        break;
    451      case 'j':
    452      case KEY_ARROW_DOWN:
    453        cm_move(&self, 1, 0);
    454        break;
    455      case 'k':
    456      case KEY_ARROW_UP:
    457        cm_move(&self, -1, 0);
    458        break;
    459      case 'l':
    460      case KEY_ARROW_RIGHT:
    461        cm_move(&self, 0, 1);
    462        break;
    463      case 'h':
    464      case KEY_ARROW_LEFT:
    465        cm_move(&self, 0, -1);
    466        break;
    467      case ' ':
    468        // start timer
    469        if (self.time == -1) {
    470          self.time = time(NULL);
    471        }
    472        cm_open(&self, self.cursor.y, self.cursor.x);
    473        break;
    474      case 'f':
    475        cm_flag(&self, self.cursor.y, self.cursor.x);
    476        break;
    477      default:
    478        break;
    479    }
    480  }
    481  term_write(&term, "\x1b[?1049l");
    482  term_flush(&term);
    483  term_rawmode_disable(&term);
    484  term_cleanup(&term);
    485  return EXIT_SUCCESS;
    486}