Engine.cc (15537B)
1 #include "Engine.hpp" 2 3 enum Color { 4 WALL = 1, 5 PATH, 6 POTTER, 7 GNOME, 8 TRAAL, 9 STONE, 10 PARCHMENT, 11 LAST 12 }; 13 14 Engine::Engine(const char *mapfile, const char *scorefile, const char *name) 15 { 16 /* 17 * We'll use std::runtime_error exceptions here because we 18 * want to display a useful error message since there are 19 * many points of failure. 20 * If we do catch an exception, we'll just "forward" it 21 * to `main`. 22 */ 23 try { 24 /* 25 * Initialize curses(3) first so we can get the terminal's 26 * dimensions and use them in `load_map`. 27 */ 28 init_curses(); 29 load_map(mapfile); 30 score = new Score(scorefile, name); 31 init_gamewin(); 32 } catch (const std::ios_base::failure& e) { 33 /* 34 * Kill the curses window, otherwise the terminal output 35 * will be messed up. 36 */ 37 (void)endwin(); 38 throw std::runtime_error("error: " + std::string(e.what())); 39 } catch (const std::runtime_error& e) { 40 (void)endwin(); 41 throw std::runtime_error("error: " + std::string(e.what())); 42 } 43 reset_entities(); 44 45 init_popup_msgs(); 46 /* Display a welcome message. */ 47 popup(p_rules); 48 f_running = 1; 49 } 50 51 Engine::~Engine() 52 { 53 delete score; 54 free_entities(); 55 map.clear(); 56 p_ctrls.clear(); 57 p_rules.clear(); 58 p_win.clear(); 59 p_lose.clear(); 60 (void)delwin(gw); 61 (void)endwin(); 62 } 63 64 /* 65 * Clean up all moving entities and gems. 66 */ 67 void 68 Engine::free_entities() 69 { 70 player = nullptr; 71 for (auto&& e : entities) 72 delete e; 73 for (auto&& g : gems) 74 delete g; 75 if (prch != nullptr) 76 delete prch; 77 entities.clear(); 78 gems.clear(); 79 } 80 81 void 82 Engine::reset_entities() 83 { 84 free_entities(); 85 init_entities(); 86 prch = nullptr; 87 } 88 89 /* 90 * Initialize curses(3) environment 91 */ 92 void 93 Engine::init_curses() 94 { 95 std::vector<int> colors; 96 97 if (!initscr()) 98 throw std::runtime_error("init_curses failed"); 99 /* Don't echo keypresses. */ 100 noecho(); 101 /* Disable line buffering. */ 102 cbreak(); 103 /* Hide the cursor. */ 104 curs_set(false); 105 /* Allow arrow-key usage. */ 106 keypad(stdscr, true); 107 /* ESC has a small delay after it's pressed, so we'll remove it. */ 108 set_escdelay(0); 109 /* 110 * Don't wait for a keypress -- just continue if there's no keypress 111 * within 1000 milliseconds. We could set the delay to 0 milliseconds, 112 * but this will most likely burn the CPU. 113 */ 114 timeout(1000); 115 (void)getmaxyx(stdscr, ymax, xmax); 116 117 /* 118 * This has to follow the same order as the enum declaration at 119 * the top of the file. Sadly, we cannot use C99's designated 120 * initialization. 121 */ 122 colors = { 123 COLOR_BLUE, /* Wall */ 124 COLOR_RED, /* Path */ 125 COLOR_CYAN, /* Potter */ 126 COLOR_GREEN, /* Gnome */ 127 COLOR_YELLOW, /* Traal */ 128 COLOR_WHITE, /* Stone */ 129 COLOR_BLACK, /* Parchment */ 130 }; 131 132 start_color(); 133 /* Use the terminal's colorscheme. */ 134 use_default_colors(); 135 for (int i = 1; i < Color::LAST; i++) 136 (void)init_pair(i, colors[i-1], -1); 137 } 138 139 /* 140 * Initiliaze the game window. Having a seperate window for the game 141 * will make it easier to handle the map and player input. 142 */ 143 void 144 Engine::init_gamewin() 145 { 146 int wr, wc, wy, wx; 147 148 wr = h; 149 wc = w; 150 wy = CENTER(ymax, wr); 151 wx = CENTER(xmax, wc); 152 if ((gw = newwin(wr, wc, wy, wx)) == NULL) 153 throw std::runtime_error("init_gamewin failed"); 154 box(gw, 0, 0); 155 (void)getmaxyx(gw, wymax, wxmax); 156 } 157 158 void 159 Engine::load_map(const char *mapfile) 160 { 161 std::ifstream f; 162 std::string str; 163 std::size_t l; 164 int curline = 1; 165 166 f.exceptions(std::ifstream::badbit); 167 f.open(mapfile); 168 if (!f.is_open()) 169 throw std::runtime_error(std::string(mapfile) + 170 ": cannot open file"); 171 /* 172 * Read first row outside the loop so we can get an initial 173 * row length. 174 */ 175 if (!std::getline(f, str)) 176 throw std::runtime_error(std::string(mapfile) + 177 ": cannot read first row"); 178 map.push_back(str); 179 l = str.length(); 180 while (std::getline(f, str)) { 181 /* 182 * If a row happens to have a different length, the map hasn't 183 * been written properly, so we exit. All rows have to 184 * have the same length. 185 */ 186 if (l != str.length()) 187 throw std::runtime_error(std::string(mapfile) + 188 ": rows must have an equal length: line " + 189 std::to_string(curline)); 190 191 /* 192 * The map must not contain anything other than SYM_PATH 193 * and SYM_WALL. 194 */ 195 for (char& c : str) 196 if (c != SYM_PATH && c != SYM_WALL) 197 throw std::runtime_error(std::string(mapfile) + 198 "the map must contain only spaces and " 199 "asterisks: line: " + 200 std::to_string(curline)); 201 202 map.push_back(str); 203 curline++; 204 } 205 f.close(); 206 /* 207 * Since we got here, we know that number of columns is the same for 208 * every row -- we can now just take a random string and calculate its 209 * size in order to get the map's width. 210 */ 211 w = map[0].length(); 212 h = map.size(); 213 214 /* 215 * The map has to fit in the screen, obviously. The top 2 lines on 216 * the Y axis are reserved for the status bar. 217 */ 218 if (w > xmax || h > ymax - 2) 219 throw std::runtime_error(std::string(mapfile) + 220 ": the map doesn't fit to screen"); 221 } 222 223 bool 224 Engine::collides_with_wall(int x, int y) const 225 { 226 if (x < w && y < h) 227 return map[y][x] == SYM_WALL; 228 return true; 229 } 230 231 /* 232 * Calculate a new position for an entity or gem. 233 */ 234 void 235 Engine::calc_pos(int *x, int *y) 236 { 237 do { 238 *x = rand() % w; 239 *y = rand() % h; 240 /* 241 * Don't spawn at the same coordinates with another entity 242 * or gem. 243 */ 244 for (const auto& e : entities) 245 if (*x == e->get_x() && *y == e->get_y()) 246 continue; 247 for (const auto& g : gems) 248 if (*x == g->get_x() && *y == g->get_y()) 249 continue; 250 } while (collides_with_wall(*x, *y)); 251 } 252 253 void 254 Engine::init_entities() 255 { 256 int i, x, y, type; 257 258 srand(time(nullptr)); 259 calc_pos(&x, &y); 260 entities.push_back(new Potter(x, y, Movable::Direction::DOWN, SYM_POTTER)); 261 for (i = 0; i < nenemies; i++) { 262 calc_pos(&x, &y); 263 /* 264 * Randomly choose whether we'll create a `Gnome` or 265 * `Traal` enemy. 266 */ 267 switch (type = rand() % 2) { 268 case 0: 269 entities.push_back(new Gnome(x, y, 270 Movable::Direction::DOWN, SYM_GNOME)); 271 break; 272 case 1: 273 entities.push_back(new Traal(x, y, 274 Movable::Direction::DOWN, SYM_TRAAL)); 275 break; 276 } 277 } 278 for (i = 0; i < ngems; i++) { 279 calc_pos(&x, &y); 280 gems.push_back(new Gem(x, y, SYM_STONE)); 281 } 282 283 /* Potter is *always* the first entry in `entities`. */ 284 player = (Potter *)entities[0]; 285 } 286 287 void 288 Engine::spawn_parchment() 289 { 290 int x, y; 291 292 calc_pos(&x, &y); 293 prch = new Gem(x, y, SYM_PARCHMENT); 294 } 295 296 /* 297 * Draw a popup window with the `lines` argument as contents. 298 */ 299 int 300 Engine::popup(const std::vector<std::string>& lines) const 301 { 302 WINDOW *win; 303 auto lencmp = [](const std::string& a, const std::string& b) { 304 return a.length() < b.length(); 305 }; 306 std::size_t vecsz; 307 int wr, wc, wy, wx; 308 int ch; 309 310 vecsz = lines.size(); 311 /* 312 * Find longest string to set the right window width. +2 columns 313 * for the box. 314 */ 315 wc = std::max_element(lines.begin(), lines.end(), lencmp)->length() + 2; 316 /* 317 * +2 lines for the box, and +1 for the space between the last message 318 * and the rest of the messages. 319 */ 320 wr = vecsz + 3; 321 wx = CENTER(xmax, wc); 322 wy = CENTER(ymax, wr); 323 if ((win = newwin(wr, wc, wy, wx)) == NULL) 324 return ERR; 325 werase(win); 326 box(win, 0, 0); 327 for (std::size_t i = 0; i < vecsz; i++) { 328 if (i != vecsz - 1) 329 mvwprintw(win, i + 1, 1, lines[i].c_str()); 330 /* 331 * The last message is always the "quit menu" message, so 332 * we'll leave an empty line before we print it. 333 */ 334 else 335 mvwprintw(win, i + 2, 1, lines[i].c_str()); 336 } 337 wrefresh(win); 338 339 /* Save the key we pressed -- it's useful in `round_end`. */ 340 ch = wgetch(win); 341 werase(win); 342 wrefresh(win); 343 (void)delwin(win); 344 345 return ch; 346 } 347 348 void 349 Engine::kbd_input() 350 { 351 int key, dir, newx, newy; 352 353 newx = player->get_x(); 354 newy = player->get_y(); 355 /* g++ was complaining `dir` wasn't initialized. */ 356 dir = 0; 357 358 switch (key = getch()) { 359 case KEY_LEFT: 360 newx--; 361 dir = Movable::Direction::LEFT; 362 break; 363 case KEY_RIGHT: 364 newx++; 365 dir = Movable::Direction::RIGHT; 366 break; 367 case KEY_UP: 368 newy--; 369 dir = Movable::Direction::UP; 370 break; 371 case KEY_DOWN: 372 newy++; 373 dir = Movable::Direction::DOWN; 374 break; 375 case 'c': 376 (void)popup(p_ctrls); 377 return; 378 case 's': 379 (void)popup(score->scores_strfmt()); 380 break; 381 case 'r': 382 /* Reset the score as well. */ 383 upd_score(-score->get_curscore()); 384 reset_entities(); 385 break; 386 case ESC: /* FALLTHROUGH */ 387 f_running = 0; 388 default: 389 /* 390 * If no key was pressed, just return -- we 391 * don't want to move the player. 392 */ 393 return; 394 } 395 396 if (!collides_with_wall(newx, newy)) 397 player->set_newpos(dir, wxmax, wymax); 398 } 399 400 /* 401 * Calculate the Eucledean 2D distance from an enemy to the player. 402 * 403 * Each new distance calculated is added to the `dists` map, which 404 * contains the distance and also a direction associated with it. 405 */ 406 void 407 Engine::calc_dist(std::map<int, int>& dists, int ex, int ey, int dir) const 408 { 409 int px, py, dx, dy, d; 410 411 px = player->get_x(); 412 py = player->get_y(); 413 dx = ex - px; 414 dy = ey - py; 415 d = floor(sqrt(dx * dx + dy * dy)); 416 dists.insert(std::pair<int, int>(d, dir)); 417 } 418 419 /* 420 * Definitely not a sophisticated pathfinding algorithm, but the way it 421 * works is this: 422 * 423 * 1. For each possible direction (west, east, north, south), see if 424 * a movement is possible in the first place -- i.e we won't hit a 425 * wall. 426 * 2. Calculate the Eucledean distance for each possible direction 427 * and save it in a map (distance, direction). 428 * 3. Sort the map, and get the minimum distance, this is the shortest 429 * path to the player. 430 * 4. Get the direction from the minimum pair in the map and go there. 431 */ 432 void 433 Engine::enemies_move() 434 { 435 std::map<int, int> dists; 436 int ex, ey; 437 auto distcmp = [](const std::pair<int, int>& a, const std::pair<int, int>& b) { 438 return a.first < b.second; 439 }; 440 441 for (const auto& e : entities) { 442 if (e == player) 443 continue; 444 ex = e->get_x(); 445 ey = e->get_y(); 446 /* Clear previous entity's data. */ 447 dists.clear(); 448 /* West */ 449 if (!collides_with_wall(ex - 1, ey)) 450 calc_dist(dists, ex - 1, ey, Movable::Direction::LEFT); 451 /* East */ 452 if (!collides_with_wall(ex + 1, ey)) 453 calc_dist(dists, ex + 1, ey, Movable::Direction::RIGHT); 454 /* North */ 455 if (!collides_with_wall(ex, ey - 1)) 456 calc_dist(dists, ex, ey - 1, Movable::Direction::UP); 457 /* South */ 458 if (!collides_with_wall(ex, ey + 1)) 459 calc_dist(dists, ex, ey + 1, Movable::Direction::DOWN); 460 461 /* 462 * If `dists` is not empty, it means we have found at 463 * least one valid movement. 464 */ 465 if (!dists.empty()) { 466 auto min = std::min_element(dists.begin(), 467 dists.end(), distcmp); 468 e->set_newpos(min->second, wxmax, wymax); 469 } 470 } 471 } 472 473 /* 474 * See if the player collides with either an enemy, gem or the parchment. 475 */ 476 void 477 Engine::collisions() 478 { 479 int px, py, x, y; 480 481 px = player->get_x(); 482 py = player->get_y(); 483 484 /* Collision with an enemy. */ 485 for (const auto& e : entities) { 486 x = e->get_x(); 487 y = e->get_y(); 488 if (e != player && px == x && py == y) 489 round_end(false); 490 } 491 492 /* Collision with a gem. */ 493 for (auto& g : gems) { 494 x = g->get_x(); 495 y = g->get_y(); 496 if (px == x && py == y) { 497 upd_score(SCORE_STONE); 498 delete g; 499 /* If we hit a gem, remove it from the vector. */ 500 gems.erase(std::remove(gems.begin(), gems.end(), g), 501 gems.end()); 502 } 503 } 504 505 /* 506 * The parchment has been spawned, if we collide with 507 * it, we won the round. 508 */ 509 if (gems.empty() && prch != nullptr) { 510 x = prch->get_x(); 511 y = prch->get_y(); 512 if (px == x && py == y) { 513 upd_score(SCORE_PARCHMENT); 514 delete prch; 515 round_end(true); 516 } 517 /* 518 * If the `gems` vector is empty, we need to spawn the 519 * parchment. 520 */ 521 } else if (gems.empty() && prch == nullptr) 522 spawn_parchment(); 523 } 524 525 /* 526 * Update the score after each round. 527 */ 528 void 529 Engine::upd_score(int n) 530 { 531 *score << score->get_curname() << score->get_curscore() + n; 532 } 533 534 /* 535 * Let the user choose if he wants to start a new round or exit the game. 536 */ 537 void 538 Engine::round_end(bool is_win) 539 { 540 int ch; 541 542 /* 543 * If we lost, reset the score in case the user starts a new 544 * round. We keep the score only if the player wins the round. 545 */ 546 if (!is_win) 547 upd_score(-score->get_curscore()); 548 /* If we won, increase the number of enemies. */ 549 else 550 nenemies++; 551 552 /* 553 * Get out of here only if the user presses 'n' or 'q' 554 * because it's very easy to accidentally mislick and 555 * exit the game. 556 */ 557 for (;;) { 558 switch (ch = popup(is_win ? p_win : p_lose)) { 559 case 'n': 560 reset_entities(); 561 return; 562 case 'q': 563 f_running = 0; 564 return; 565 } 566 } 567 } 568 569 void 570 Engine::redraw() const 571 { 572 const char msg_pos[] = "Potter: (%d, %d)"; 573 const char msg_score[] = "Score: %d (%s)"; 574 const char msg_opts[] = "c Controls"; 575 576 werase(gw); 577 erase(); 578 579 /* Draw top bar info. */ 580 mvprintw(0, 0, msg_pos, player->get_x(), player->get_y()); 581 mvprintw(0, CENTER(xmax, strlen(msg_score)), msg_score, 582 score->get_curscore(), score->get_curname()); 583 mvprintw(0, xmax - strlen(msg_opts), msg_opts); 584 mvhline(1, 0, ACS_HLINE, xmax); 585 586 /* Draw everything. */ 587 wattron(gw, A_REVERSE); 588 draw_map(); 589 draw_entities(); 590 draw_gems(); 591 if (prch != nullptr) 592 draw_parchment(); 593 wattroff(gw, A_REVERSE); 594 refresh(); 595 wrefresh(gw); 596 } 597 598 void 599 Engine::draw_map() const 600 { 601 int color; 602 603 /* 604 * Even though the map is stored as a `std::vector<std::string>`, 605 * we'll loop through it character by character so we can set 606 * the colors. 607 */ 608 for (const auto& row : map) { 609 for (const auto& c : row) { 610 if (c == SYM_WALL) 611 color = COLOR_PAIR(Color::WALL); 612 else if (c == SYM_PATH) 613 color = COLOR_PAIR(Color::PATH); 614 wattron(gw, color); 615 waddch(gw, c); 616 wattroff(gw, color); 617 } 618 } 619 } 620 621 void 622 Engine::draw_entities() const 623 { 624 int color; 625 626 for (const auto& e : entities) { 627 /* Determine subclass type and assign colors accordingly. */ 628 if (dynamic_cast<Potter *>(e) != nullptr) 629 color = COLOR_PAIR(Color::POTTER); 630 else if (dynamic_cast<Gnome *>(e) != nullptr) 631 color = COLOR_PAIR(Color::GNOME); 632 else if (dynamic_cast<Traal *>(e) != nullptr) 633 color = COLOR_PAIR(Color::TRAAL); 634 wattron(gw, color); 635 mvwaddch(gw, e->get_y(), e->get_x(), e->get_sym()); 636 wattroff(gw, color); 637 } 638 } 639 640 void 641 Engine::draw_gems() const 642 { 643 int color; 644 645 for (const auto& g : gems) { 646 if (g->get_sym() == SYM_STONE) 647 color = COLOR_PAIR(Color::STONE); 648 else if (g->get_sym() == SYM_PARCHMENT) 649 color = COLOR_PAIR(Color::PARCHMENT); 650 wattron(gw, color); 651 mvwaddch(gw, g->get_y(), g->get_x(), g->get_sym()); 652 wattroff(gw, color); 653 } 654 } 655 656 void 657 Engine::draw_parchment() const 658 { 659 wattron(gw, COLOR_PAIR(Color::PARCHMENT)); 660 mvwaddch(gw, prch->get_y(), prch->get_x(), prch->get_sym()); 661 wattroff(gw, COLOR_PAIR(Color::PARCHMENT)); 662 } 663 664 bool 665 Engine::is_running() const 666 { 667 return f_running; 668 } 669 670 /* Initialize messages for the popup windows. */ 671 void 672 Engine::init_popup_msgs() 673 { 674 p_ctrls = { 675 "Up Move up", 676 "Down Move down", 677 "Left Move left", 678 "Right Move right", 679 "ESC Quit", 680 "s High Scores", 681 "c Controls menu", 682 "r Restart game", 683 "Press any key to quit the menu", 684 }; 685 p_rules = { 686 " Babis Potter", 687 "--------------------------------------------------------", 688 "The objective is to collect all the gems without getting", 689 "caught by the Gnomes and Traals!", 690 "", 691 "You can always see what controls are available by", 692 "pressing the 'c' key.", 693 " Press any key to continue", 694 }; 695 p_win = { 696 " You won!", 697 "Press 'n' to play a new round or 'q' to quit.", 698 }; 699 p_lose = { 700 " You lost!", 701 "Press 'n' to play a new round or 'q' to quit.", 702 }; 703 }