gol

Implementation of Conway's Game of Life writen in C
git clone git://git.dimitrijedobrota.com/gol.git
Log | Files | Refs | README

game.c (16932B)


      1 /**
      2  * @file game.c
      3  * @author Dimitrije Dobrota
      4  * @date 16 June 2022
      5  * @brief This file handles displaying and managing game using logic module
      6  *
      7  * This file contain function for both managing and displaying the game
      8  * state. Apart from the main game runner that relies on logic module there are
      9  * function for displaying game, cursor, and status. It takes care of screen and
     10  * cursor position as well as handle mouse input.
     11  */
     12 
     13 #include <curses.h>
     14 #include <time.h>
     15 
     16 #include "display.h"
     17 #include "game.h"
     18 #include "logic.h"
     19 #include "main.h"
     20 #include "utils.h"
     21 #include "window.h"
     22 
     23 #define DEF_GEN_STEP    1
     24 #define DEF_SCREEN_STEP 1
     25 #define DEF_TIME_CONST  100
     26 #define DEF_TIME_STEP   1
     27 
     28 #ifdef _WIN32
     29 #define TIME_MOD 1
     30 #else
     31 #define TIME_MOD 1000
     32 #endif
     33 
     34 extern char *evolution_names[];
     35 extern int   evolution_cells[];
     36 extern int   evolution_size;
     37 
     38 extern window_T menu_w;
     39 extern mmask_t  mbitmask;
     40 extern Cell    *hash;
     41 
     42 typedef int (*coordinate_f)(int, int, int);
     43 
     44 /**
     45  * @brief Given a coordinate relative to the screen, return real game coordinate
     46  * if not wrapping
     47  */
     48 int coordinate_nowrap(int val, int offset, int max) { return val + offset; }
     49 
     50 /**
     51  * Given a coordinate relative to the screen, return real game coordinate
     52  * wrapping
     53  */
     54 int coordinate_wrap(int val, int offset, int max) {
     55   return (val + offset + max) % max;
     56 }
     57 
     58 coordinate_f
     59     cord; ///< function to be used to get real coordinates from relative ones
     60 
     61 int height, width;
     62 
     63 static int win_height, win_width;
     64 static int screen_offset_x, screen_offset_y;
     65 static int cursor_offset_x, cursor_offset_y;
     66 static int wrap, gen_step, screen_step;
     67 static int play, time_const, time_step;
     68 
     69 static unsigned gen;
     70 
     71 #define y_at(y) y, screen_offset_y, height
     72 #define x_at(x) x, screen_offset_x, width
     73 
     74 /**
     75  * @brief Convenience macro for looping over all the cells in a given coordinate
     76  * range
     77  */
     78 #define for_each_cell(start_i, end_i, start_j, end_j)                          \
     79   for (int i = start_i; i < end_i; i++)                                        \
     80     for (int j = start_j; j < end_j; j++)
     81 
     82 /**
     83  * @brief Display cell at given game coordinates  to the ncurses WINDOW.
     84  * Requires int val to be set to the value of the cell to be displayed
     85  */
     86 #define mvprint_cell(win, i, j, color_offset, blank)                           \
     87   {                                                                            \
     88     wmove(win, i + 1, j * 2 + 1);                                              \
     89     wattrset(win, COLOR_PAIR(val + color_offset));                             \
     90     print_cell(win, blank);                                                    \
     91   }
     92 
     93 /**
     94  * @brief Given a coordinate range, display that part of the game to the ncurses
     95  * WINDOW. Use color scheme defined in color_offset, with dead cells being
     96  * filled with blank
     97  */
     98 #define print_cells(win, start_i, end_i, start_j, end_j, color_offset, blank)  \
     99   for (int i = start_i; i < end_i; i++) {                                      \
    100     wmove(win, i + 1, 1 + start_j * 2);                                        \
    101     for (int j = start_j; j < end_j; j++) {                                    \
    102       int val = getAt(cord(y_at(i)), cord(x_at(j)));                           \
    103       wattrset(win, COLOR_PAIR(val + color_offset));                           \
    104       print_cell(win, blank);                                                  \
    105     }                                                                          \
    106   }
    107 
    108 /**
    109  * @brief Given a coordinate, screen offset, screen size, and board size
    110  * calculate the exact position on the screen or return a negative number if
    111  * it shouldn't be displayed.
    112  *
    113  * @param coordinate: x or y coordinates of a point to be checked
    114  * @param screen_offset: screen offset along the x or y axis
    115  * @param screen_size: screen size on the x or y axis
    116  * @param board_size: game size of x or y axis, needed if wrapping
    117  */
    118 int get_screen_position(int value, int screen_offset, int screen_size,
    119                         int board_size) {
    120   int overshoot = screen_offset + screen_size - board_size;
    121 
    122   if (wrap) {
    123     if (overshoot > 0) {
    124       if (value < screen_offset && value >= overshoot)
    125         return -1;
    126       if (value >= screen_offset) {
    127         return value - screen_offset;
    128       } else {
    129         return value + screen_size - overshoot;
    130       }
    131     } else {
    132       if (value < screen_offset || value >= screen_offset + screen_size)
    133         return -2;
    134       return value - screen_offset;
    135     }
    136   } else {
    137     if (value < screen_offset || value >= screen_offset + screen_size)
    138       return -3;
    139     return value - screen_offset;
    140   }
    141 }
    142 
    143 /**
    144  * @brief Display the part of the game seen by screen to the ncurses WINDOW
    145  * provided
    146  */
    147 void display_game(window_T wind) {
    148   WINDOW *win = window_win(wind);
    149 
    150   window_clear_noRefresh(wind);
    151 
    152   int row, col, val;
    153   for (Cell *c = hash; c != NULL; c = c->hh.next) {
    154     wattrset(win, COLOR_PAIR(val + 2));
    155 
    156     row = get_screen_position(c->cord.row, screen_offset_y, win_height, height);
    157     col = get_screen_position(c->cord.col, screen_offset_x, win_width, width);
    158     val = c->val;
    159 
    160     if (row < 0 || col < 0)
    161       continue;
    162 
    163     mvprint_cell(win, row, col, 2, CHAR_BLANK);
    164   }
    165 }
    166 
    167 /**
    168  * @brief Display the cursor, fixing the previous position, to the ncurses
    169  * WINDOW provided
    170  */
    171 void display_cursor(WINDOW *win) {
    172   static int prev_x = 0, prev_y = 0;
    173   int        val;
    174 
    175   val = getAt(cord(y_at(prev_y)), cord(x_at(prev_x)));
    176   mvprint_cell(win, prev_y, prev_x, 2, CHAR_BLANK);
    177 
    178   val = getAt(cord(y_at(cursor_offset_y)), cord(x_at(cursor_offset_x)));
    179   mvprint_cell(win, cursor_offset_y, cursor_offset_x, 5, CHAR_CURSOR);
    180 
    181   prev_y = cursor_offset_y;
    182   prev_x = cursor_offset_x;
    183 }
    184 
    185 /**
    186  * @brief Display game information to the ncurses WINDOW provided
    187  */
    188 void display_status(window_T wind) {
    189   WINDOW *win = window_win(wind);
    190 
    191   wmove(win, 1, 1);
    192   wprintw(win, " %5s | ", play ? "play" : "pause");
    193   wprintw(win, wrap ? "Size: %9dx%9d | " : "Size: unlimited | ", height, width);
    194   wprintw(win, "Generation: %10u(+%d) | ", gen, gen_step);
    195   wprintw(win, "dt: %4dms | ", time_const);
    196   wprintw(win, "Cursor: %10dx%10d | ", cord(y_at(cursor_offset_y)),
    197           cord(x_at(cursor_offset_x)));
    198   wrefresh(win);
    199 }
    200 
    201 /**
    202  * @brief Display the visual select mode
    203  *
    204  * This function is interactive:
    205  * - Use the wasd to move the selection around
    206  * - Use x to delete all selected cells
    207  * - Use t to toggle all selected cells
    208  * - Use enter to save the selected cells as a pattern
    209  * - Use q or esc to quit visual select mode
    210  */
    211 int display_select(window_T wind) {
    212   int CLINES = LINES, CCOLS = COLS;
    213 
    214   int current_offset_y = cursor_offset_y;
    215   int current_offset_x = cursor_offset_x;
    216   int ret_value = 1;
    217 
    218   WINDOW *win = window_win(wind);
    219   WINDOW *new;
    220 
    221   if (UNICODE) {
    222     new = window_win_new(wind);
    223     overlay(win, new);
    224     wrefresh(new);
    225   } else {
    226     new = win;
    227   }
    228 
    229   int ph = window_height(wind), pw = window_width(wind) / 2;
    230   nodelay(stdscr, 0);
    231   while (TRUE) {
    232     int start_i = MIN(cursor_offset_y, current_offset_y);
    233     int end_i = MAX(cursor_offset_y, current_offset_y);
    234     int start_j = MIN(cursor_offset_x, current_offset_x);
    235     int end_j = MAX(cursor_offset_x, current_offset_x);
    236 
    237     if (!UNICODE)
    238       display_game(wind);
    239 
    240     print_cells(new, start_i, end_i + 1, start_j, end_j + 1, 8, CHAR_BLANK);
    241     wrefresh(new);
    242 
    243     if (is_term_resized(CLINES, CCOLS)) {
    244       HANDLE_RESIZE;
    245       goto end;
    246     }
    247 
    248     int c = getch();
    249     switch (c) {
    250     // offset selection
    251     case 'w':
    252     case 'W':
    253     case KEY_UP:
    254       current_offset_y--;
    255       break;
    256     case 's':
    257     case 'S':
    258     case KEY_DOWN:
    259       current_offset_y++;
    260       break;
    261     case 'a':
    262     case 'A':
    263     case KEY_LEFT:
    264       current_offset_x--;
    265       break;
    266     case 'd':
    267     case 'D':
    268     case KEY_RIGHT:
    269       current_offset_x++;
    270       break;
    271 
    272     // delete selection
    273     case 'x':
    274     case 'X':
    275       for_each_cell(start_i, end_i + 1, start_j, end_j + 1)
    276           deleteAt(cord(y_at(i)), cord(x_at(j)));
    277       goto end;
    278 
    279     // toggle selection
    280     case 't':
    281     case 'T':
    282       for_each_cell(start_i, end_i + 1, start_j, end_j + 1)
    283           toggleAt(cord(y_at(i)), cord(x_at(j)));
    284       goto end;
    285 
    286     // confirm and save selection
    287     case '\n':
    288       for_each_cell(start_i, end_i + 1, start_j, end_j + 1)
    289           saveCell(cord(y_at(i)), cord(x_at(j)));
    290       ret_value = 100;
    291       goto end;
    292 
    293     // quit
    294     case 27:
    295     case 'q':
    296     case 'Q':
    297       flushinp();
    298       ret_value = 1;
    299       goto end;
    300     }
    301     flushinp();
    302 
    303     CLAMP(current_offset_y, 0, ph - 1);
    304     CLAMP(current_offset_x, 0, pw - 1);
    305 
    306     wclear(new);
    307     overlay(win, new);
    308   }
    309 end:;
    310 
    311   if (UNICODE)
    312     delwin(new);
    313 
    314   nodelay(stdscr, 1);
    315   return ret_value;
    316 }
    317 
    318 /**
    319  * @brief save the current screen and cursor coordinates
    320  * before resize to use them after redraw. Must be used before goto redraw
    321  */
    322 #define save_state()                                                           \
    323   {                                                                            \
    324     t_y = cord(y_at(win_height / 2));                                          \
    325     t_x = cord(x_at(win_width / 2));                                           \
    326     ct_y = cord(y_at(cursor_offset_y));                                        \
    327     ct_x = cord(x_at(cursor_offset_x));                                        \
    328   }
    329 
    330 /**
    331  * @brief Main game runner. Connection between logic and display
    332  * This function is responsive:
    333  * - On every resize:
    334  *   - window tree is recreated
    335  *   - new screen size is calculated with fixed center
    336  *   - cursor is clipped to the screen
    337  *   - game is redrawn
    338  *
    339  * This function is interactive:
    340  * - Use p to play/pause the simulation
    341  * - Use the arrow keys to move the screen around
    342  * - Use -/+ to decrease or increase the numbs of evolutions before displaying
    343  * change
    344  * - Use [/] to decrease or increase time wait before update
    345  * - Use q or esc to return to the main menu
    346  * - If not play:
    347  *   - Use wasd to move the cursor around
    348  *   - Use space or mouse click to toggle cell state
    349  *   - Use v to enter the visual select mode
    350  *   - Use l to load the pattern
    351  *   - Use o to save the current game
    352  *   - Use h to show help menu
    353  *   - Use r to redraw the screen
    354  */
    355 void game(int s_h, int s_w, int mode_index) {
    356   char *mode_name = evolution_names[mode_index];
    357 
    358   int t_y = 0, t_x = 0, ct_x = 0, ct_y = 0;
    359   wrap = 1;
    360 
    361   window_T status_w, screen_w, game_w;
    362 
    363   gen = 0;
    364   gen_step = DEF_GEN_STEP, time_const = DEF_TIME_CONST;
    365   time_step = DEF_TIME_STEP, screen_step = DEF_SCREEN_STEP;
    366 
    367 reset_screen:
    368   status_w = window_split(menu_w, 1, 3, 0, "Status", "Game");
    369   screen_w = window_sibiling(status_w);
    370   window_set_title(menu_w, NULL);
    371   window_clear(menu_w);
    372 
    373   wrap = (s_w > 0 && s_h > 0);
    374 
    375   if (wrap) {
    376     width = s_w;
    377     height = s_h;
    378   } else {
    379     width = 0;
    380     height = 0;
    381   }
    382 
    383   logic_init(wrap, mode_index);
    384 
    385   cord = wrap ? coordinate_wrap : coordinate_nowrap;
    386   game_w = window_center(screen_w, height, width * 2, mode_name);
    387 
    388 redraw:;
    389   int     CLINES = LINES, CCOLS = COLS;
    390   clock_t start_t, end_t = 0, total_t;
    391   MEVENT  mort;
    392 
    393   if (!wrap) {
    394     game_w = window_center(screen_w, window_height(screen_w),
    395                            window_width(screen_w), mode_name);
    396   }
    397 
    398   WINDOW *game_W = window_win(game_w);
    399   win_height = window_height(game_w), win_width = window_width(game_w) / 2;
    400 
    401   window_clear(menu_w);
    402   window_clear(screen_w);
    403   window_clear(status_w);
    404 
    405   screen_offset_y = t_y - win_height / 2;
    406   screen_offset_x = t_x - win_width / 2;
    407 
    408   if (wrap) {
    409     screen_offset_x = (screen_offset_x + width) % width;
    410     screen_offset_y = (screen_offset_y + height) % height;
    411   }
    412 
    413   cursor_offset_y = ct_y - screen_offset_y;
    414   cursor_offset_x = ct_x - screen_offset_x;
    415 
    416   if (wrap) {
    417     cursor_offset_x = (cursor_offset_x + width) % width;
    418     cursor_offset_y = (cursor_offset_y + height) % height;
    419   }
    420 
    421   CLAMP(cursor_offset_y, 0, win_height - 1);
    422   CLAMP(cursor_offset_x, 0, win_width - 1);
    423 
    424   display_game(game_w);
    425   display_cursor(game_W);
    426   wrefresh(game_W);
    427 
    428   int screen_change = 1;
    429   int cursor_change = 1;
    430 
    431   while (TRUE) {
    432     start_t = clock();
    433 
    434     if (wrap) {
    435       screen_offset_x = (screen_offset_x + width) % width;
    436       screen_offset_y = (screen_offset_y + height) % height;
    437     }
    438 
    439     if (play) {
    440       do_evolution(gen_step);
    441       screen_change = 1;
    442       gen += gen_step;
    443     }
    444 
    445     display_status(status_w);
    446 
    447     if (screen_change) {
    448       display_game(game_w);
    449       screen_change = 0;
    450       cursor_change = 1;
    451     }
    452 
    453     if (cursor_change) {
    454       display_cursor(game_W);
    455       wrefresh(game_W);
    456       cursor_change = 0;
    457     }
    458 
    459     while ((total_t = (long int)(end_t - start_t)) < time_const * TIME_MOD) {
    460       int c = getch();
    461       switch (c) {
    462 
    463       // toggle pause
    464       case 'p':
    465       case 'P':
    466         play = !play;
    467         break;
    468 
    469       // quit
    470       case 27:
    471       case 'q':
    472       case 'Q':
    473         flushinp();
    474         goto end;
    475 
    476       // change num of evolutions before display
    477       case '+':
    478         gen_step++;
    479         break;
    480       case '-':
    481         gen_step--;
    482         break;
    483 
    484       // change refresh rate
    485       case ']':
    486         time_const += time_step;
    487         break;
    488       case '[':
    489         time_const -= time_step;
    490         break;
    491       }
    492 
    493       if (!play) {
    494         // move cursor around
    495         switch (c) {
    496         case 'w':
    497         case 'W':
    498           cursor_offset_y--;
    499           cursor_change = 1;
    500           break;
    501         case 's':
    502         case 'S':
    503           cursor_offset_y++;
    504           cursor_change = 1;
    505           break;
    506         case 'a':
    507         case 'A':
    508           cursor_offset_x--;
    509           cursor_change = 1;
    510           break;
    511         case 'd':
    512         case 'D':
    513           cursor_offset_x++;
    514           cursor_change = 1;
    515           break;
    516 
    517 #if defined NCURSES_MOUSE_VERSION && !defined NO_MOUSE
    518         // mouse  toggle cell
    519         case KEY_MOUSE:
    520           getmouse(&mort);
    521           if (!window_clicked(game_w, mort.y, mort.x))
    522             break;
    523 
    524           int mouse_offset_y = mort.y - window_y(game_w) - 1;
    525           int mouse_offset_x = (mort.x - window_x(game_w) - 1) / 2;
    526           int val =
    527               toggleAt(cord(y_at(mouse_offset_y)), cord(x_at(mouse_offset_x)));
    528 
    529           if (mouse_offset_x != cursor_offset_x ||
    530               mouse_offset_y != cursor_offset_y) {
    531             mvprint_cell(game_W, mouse_offset_y, mouse_offset_x, 2, CHAR_BLANK);
    532             wrefresh(game_W);
    533           } else
    534             cursor_change = 1;
    535           break;
    536 #endif
    537 
    538         // toggle cell
    539         case ' ':
    540           toggleAt(cord(y_at(cursor_offset_y)), cord(x_at(cursor_offset_x)));
    541           cursor_change = 1;
    542           break;
    543 
    544         // visual selection
    545         case 'v':
    546         case 'V':
    547           if (display_select(game_w) == 100) {
    548             window_unsplit(menu_w);
    549             save_pattern();
    550           }
    551 
    552           save_state();
    553           goto reset_screen;
    554 
    555         // lead pattern
    556         case 'l':
    557         case 'L':
    558           window_unsplit(menu_w);
    559           setPosition(cord(y_at(cursor_offset_y)), cord(x_at(cursor_offset_x)));
    560           load_pattern();
    561 
    562           save_state();
    563           goto reset_screen;
    564 
    565         // save game
    566         case 'o':
    567         case 'O':
    568           window_unsplit(menu_w);
    569           save();
    570 
    571           save_state();
    572           goto reset_screen;
    573 
    574         // help menu
    575         case 'h':
    576         case 'H':
    577           window_unsplit(menu_w);
    578           display_patterns(menu_w);
    579 
    580           save_state();
    581           goto reset_screen;
    582 
    583         // redraw screen
    584         case 'r':
    585         case 'R':
    586           save_state();
    587           goto redraw;
    588         }
    589       }
    590 
    591       // move screen around
    592       switch (c) {
    593       case KEY_A1:
    594       case KEY_C1:
    595       case KEY_END:
    596       case KEY_HOME:
    597       case KEY_LEFT:
    598         screen_offset_x -= screen_step;
    599         screen_change = 1;
    600         break;
    601       case KEY_A3:
    602       case KEY_C3:
    603       case KEY_NPAGE:
    604       case KEY_PPAGE:
    605       case KEY_RIGHT:
    606         screen_offset_x += screen_step;
    607         screen_change = 1;
    608         break;
    609       }
    610 
    611       switch (c) {
    612       case KEY_A1:
    613       case KEY_A3:
    614       case KEY_HOME:
    615       case KEY_PPAGE:
    616       case KEY_UP:
    617         screen_offset_y -= screen_step;
    618         screen_change = 1;
    619         break;
    620       case KEY_C1:
    621       case KEY_C3:
    622       case KEY_DOWN:
    623       case KEY_END:
    624       case KEY_NPAGE:
    625         screen_offset_y += screen_step;
    626         screen_change = 1;
    627       }
    628 
    629       CLAMP(cursor_offset_y, 0, win_height - 1);
    630       CLAMP(cursor_offset_x, 0, win_width - 1);
    631 
    632       CLAMP(gen_step, 1, 100);
    633       CLAMP(time_const, 0, 1000);
    634 
    635       if (is_term_resized(CLINES, CCOLS)) {
    636         flushinp();
    637         save_state();
    638         HANDLE_RESIZE;
    639         goto redraw;
    640       }
    641       end_t = clock();
    642     }
    643   }
    644 end:;
    645   window_unsplit(menu_w);
    646   logic_free();
    647   return;
    648 }