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 }