uni

University stuff
git clone git://git.margiolis.net/uni.git
Log | Files | Refs | README | LICENSE

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 }