/* bugs / known issues - Makefile for project doesn't check dependencies for cs20200 properly - gdb doesn't have line-numbers, etc. for this file (depending on order of make'ing) Note - some random comments/thoughts/advice are througout. Search for "note" */ #ifndef OBJECTS_HPP #define OBJECTS_HPP using namespace std; #include<list> #include<vector> #include<fstream> #include<iostream> #include<sstream> #include<assert.h> #include<unistd.h> #include<string.h> #include<time.h> /* *** Class definitions *** */ class World; class Game; // Object - has a location, velocity, whether can move or not class Object { public: double x, vx; // x pos and velocity, kept as double rather than int so "real" position can move fractionally double y, vy; // y pos and velocity bool moves; // is this object able to move char ch; // character for displaying bool isMoving_y; // is moving in y direction or not int num; // used for different things depending on object type bool flagToRemove; // if should remove, set to true Object(char ch); // constructor void update_pos(double g, double delta_s, Game &G); // update position/velocity virtual string state(); // string summary of state }; // Player - is an object, also has coins and lives class Player : public Object { private: int lives; public: int coins; Player(); string state(); // overrides the default state function, to include coins, lives void addCoin(); int loseLife(); int getLives(); }; // World - keeps track of all objects, movement class World { public: double g, // gravity, #rows/sec down delta_s; // interval between updates to velocity/position int width, height; // of the whole world list<Object *> objs; // linked list of objects Object *** grid; // 2d array of objects Game &G; // for having access to rest of the game World(Game &G); // constructor void update(); // update positions, etc. void add_player(); // add players before adding any other objects }; // Screen - for drawing/display class Screen { public: int max_x, max_y; // dimensions of screen bool colors_ok; // were we able to initialize colors and such Game &G; Screen(Game &G); ~Screen(); // destructor, for free'ing void draw(); // draw the screen char occupied(double y, double x); // is that space occupied char barrier(double y, double x); // is that space a barrier (wall/floor) bool same_location(Object *ob1, Object *ob2); }; // Game - keeps the entire game class Game { public: World w; // keeps objects Screen s; // for drawing bool gameOver; int removedCount; // for debugging, how many objects have been removed from board time_t time_start; Game(); bool load_world(const char *filename); void play_game(); void handle_key(int ch); void moveObject(Object *obj, double new_y, double new_x); // move object, updating things as needed bool isPlayer(Object *obj); }; /* *** Class method defitions *** */ /* Object method definitions */ // ch(ch) is like this->ch = ch Object::Object(char ch) : ch(ch) { vx = 0; vy = 0; moves = false; isMoving_y = false; num = 0; flagToRemove = false; } // update position and velocities based on gravity, velocities // return false if should be removed void Object::update_pos(double g, double delta_s, Game &G) { if (moves) { // y and vy double x_new, y_new; // update y velocity based on gravity, and y position if (isMoving_y) { vy = vy + (g * delta_s); y_new = y + vy * delta_s; // where we would end up } else y_new = y; // update x position x_new = x + vx * (delta_s); // check if our new position would have run into something if (G.s.barrier(y_new, x_new)) { // can't move there vy = 0; // if y was going down, reset vy to 0 and not moving y if ((int) y_new < (int) y) { isMoving_y = false; } // if y was going up and barrier is a & then take coins if ((int) y_new > (int) y && G.s.barrier(y_new, x_new) == '&') { Object *obj = G.w.grid[(int) y_new][(int) x_new]; if (obj != NULL && obj->num > 0) { obj->num = obj->num - 1; if (obj->num == 0) obj->ch = '='; Player *p = (Player *) *(G.w.objs.begin()); p->coins ++; } } // if x was changing, make vx bounce if ((int) x_new != (int) x) vx *= -1; } else { G.moveObject(this, y_new, x_new); } } else { // for non-moving object, just put it on the grid to make sure // the grid is updated in case some moving object overwrote this one G.w.grid[(int)y][(int)x] = this; } } string Object::state() { ostringstream oss; // x, y, etc. for debugging oss << "(x, y) = (" << x << ", " << y << ") " << "(vx, vy) = (" << vx << ", " << vy << ") " << "moves=" << moves; return oss.str(); } /* Player method definitions */ Player::Player() : Object('M') { coins = 0; lives = 3; x = 0; y = 0; vy = 0; moves = true; } string Player::state() { string s = Object::state(); ostringstream oss; oss << " " << ch << ": coins=" << coins << " lives=" << lives; // skip showing x,y, etc. except for debugging //return s + oss.str(); return oss.str(); } void Player::addCoin() { coins++; } // note - good OO design - use methods to get/change state of the object, // and make the actual lives variable as private so other code doesn't // need to worry about the internal details. int Player::loseLife() { lives = lives > 0 ? lives-1 : 0; return lives; } int Player::getLives() { return lives; } /* World method definitions */ World::World(Game &G) : G(G){ // gravity - tradeoff between this and the vy that is set on a keyup g = -60; delta_s = 0.01; width = height = 0; grid = NULL; } void World::update() { // the player Player *pl = (Player *) *(objs.begin()); // update positions of objects - call the update function on each for(list<Object *>::iterator it= objs.begin(); it != objs.end(); it++) { (*it)->update_pos(g, delta_s, G); } // go through and remove the ones that were flagged for removal for(list<Object *>::iterator it= objs.begin(); it != objs.end(); ) { Object *obj = *it; if (obj->flagToRemove) { grid[(int) obj->y][(int) obj->x] = NULL; delete obj; it = objs.erase(it); } else it++; } // note - we are storing all of the obects in both the objs list and the // grid. we could just as well have iterated through the grid to call // update on all of the objects. } void World::add_player() { Player *p = new Player(); objs.push_back(p); } /* Screen method definitions */ // color definitions, see https://jonasjacek.github.io/colors/ // assuming your terminal has the same color definitions, these // choices are reasonable looking colors. #define MARIO_BLUE 19 #define MARIO_RED 160 #define MARIO_GREEN 40 #define MARIO_GOLD 221 #define MARIO_WHITE 15 #define MARIO_BLACK 0 // color in curses uses color pairs, and there are two different places where a magic // code needs to be used. define our magic codes here so we are consistent when we use // them. See below... typedef enum {MARIO_GREEN_BLUE=1, MARIO_RED_BLUE, MARIO_GOLD_BLUE, MARIO_BLACK_BLUE, MARIO_WHITE_BLUE} MARIO_COLOR_PAIRS; // Constructor for screen - get all of the curses things setup Screen::Screen(Game &G) : G(G) { // note - already done from main.c: // initsrc (creates screen) // noecho (don't display chars that are typed) // cbreak (don't wait for enter key) // curs_set(0) - makes cursor not visible // set colors_ok = false if any of the color things give an error condition colors_ok = true; if (start_color() == ERR) colors_ok = false; // it seems init_color doesn't work on some terminals (mine on mac anyway). // it seems the default colors are - https://jonasjacek.github.io/colors/ // unless you changed them, so I picked colors from the defaults above that look okay on // my terminal (still need to try on others... attron(A_BOLD); // everything bold if (init_pair(MARIO_GREEN_BLUE, MARIO_GREEN, MARIO_BLUE) == ERR) colors_ok = false; if (init_pair(MARIO_RED_BLUE, MARIO_RED, MARIO_BLUE) == ERR) colors_ok = false; if (init_pair(MARIO_GOLD_BLUE, MARIO_GOLD, MARIO_BLUE) == ERR) colors_ok = false; if (init_pair(MARIO_BLACK_BLUE, MARIO_BLACK, MARIO_BLUE) == ERR) colors_ok = false; if (init_pair(MARIO_WHITE_BLUE, MARIO_WHITE, MARIO_BLUE) == ERR) colors_ok = false; // screen setup nodelay(stdscr, TRUE); // make getch return even if no character typed keypad(stdscr, TRUE); // enable use of arrow keys. // max x and y based on the size of the screen max_x = COLS-3; max_y = LINES-3; } // descructor - cleanup Screen::~Screen() { endwin(); refresh(); } // main function for drawing the screen. note that it also // checks for collisions and such. this is probably bad OO design - should // separate out the collision detection and drawing. void Screen::draw() { // erase the screen bkgd(COLOR_PAIR(MARIO_RED_BLUE)); erase(); // border and such attron(COLOR_PAIR(MARIO_RED_BLUE)); border('|', '|', '-', '-', '+', '+', '+', '+'); attroff(COLOR_PAIR(MARIO_RED_BLUE)); // information at the top attron(COLOR_PAIR(MARIO_WHITE_BLUE)); mvprintw(1, 1,"q to quit, left/right arrows move, up arrow to jump"); // player object should be first on the objs list - get info, print about it assert(G.w.objs.size() > 0); Player *p = (Player *)(*(G.w.objs.begin())); int y, x; char ch; y = (int) p->y; x = (int) p->x; ch = p->ch; mvprintw(2, 1, "%s", p->state().c_str()); // some more information about the world mvprintw(3, 1, "world height, width ~ %d, %d, %d sec", G.w.height, G.w.width, time(NULL) - G.time_start); mvprintw(4, 1, "removedCount %d", G.removedCount); //mvprintw(4, 1, "%d, %d, %d, %d. colors_ok %d. %d", can_change_color(), has_colors(), COLORS, COLOR_PAIRS, colors_ok == true, COLOR_WHITE); attroff(COLOR_PAIR(MARIO_WHITE_BLUE)); // non-player world objects, start at 1, go through to display each one for(list<Object *>::iterator it= ++G.w.objs.begin(); it != G.w.objs.end(); ++it) { int obj_y = (int) (*it)->y, obj_x = (int) (*it)->x; char obj_ch = (*it)->ch; // what color should object be, default to red short color = MARIO_RED_BLUE; switch(obj_ch) { case '$': case '&': color = MARIO_GOLD_BLUE; break; case '@': color = MARIO_RED_BLUE; break; case '_': color = MARIO_BLACK_BLUE; break; case '=': case '|': color = MARIO_GREEN_BLUE; break; } // draw it attron(COLOR_PAIR(color)); mvprintw(max_y - (int) (*it)->y + 1, // y=0 is at bottom, switch around, leave 1 for border (int) (*it)->x + 1, // don't draw on border "%c", (*it)->ch); attroff(COLOR_PAIR(color)); } // now draw player attron(COLOR_PAIR(MARIO_WHITE_BLUE)); mvprintw(max_y - y + 1, x + 1, "%c", ch); // see comments above attroff(COLOR_PAIR(MARIO_WHITE_BLUE)); // if game over, let them know if (G.gameOver) { mvprintw(max_y/2, max_x/3, "~~~ GAME OVER ~~~"); } // nothing gets redrawn on the screen until calling refresh refresh(); } // return non-zero if x, y is hitting something (e.g. '=') char Screen::barrier(double y, double x) { // off screen, return 1 if ((int) y < 0 || (int) y >= G.w.height || (int) x < 0 || (int) x >= G.w.width) return 1; // is the spot occupied char ch = occupied(y, x); switch(ch) { case '=': case '|': case '&': case '_': return ch; } return 0; } char Screen::occupied(double y, double x) { int xx = (int)x + 1; // translate from game coordinates to screen coordinates int yy = max_y - (int)y + 1; // read that position on the screen char ch = mvinch((int) yy, (int) xx); if (ch == ' ') return 0; return ch; } bool Screen::same_location(Object *ob1, Object *ob2) { if (ob1 == NULL || ob2 == NULL) return false; if ((int) ob1->x == (int) ob2->x && (int) ob1->y == (int) ob2->y) return true; else return false; } /* Game method definitions */ // note: gameOver(false) is like gameOver = false; // w{World(*this)} is like I declared World w(*this); Game::Game() : w{World(*this)}, s{Screen(*this)}, gameOver(false), removedCount(0) { w.add_player(); // add players before loading any other objects time_start = time(NULL); } // return true if loaded, false if not bool Game::load_world(const char *filename) { ifstream in(filename); // somewhere in there it does something like fopen or open vector<string> lines; // check that file was opened if (! in.good()) { cout << "Unable to open " << filename << " for reading." << endl; return false; } // loop to read from input, into vector (aka array) of lines string line; unsigned max_len = 0; while (getline(in, line)) { if (line.length() > max_len) max_len = line.length(); lines.push_back(line); } in.close(); // height and width of the world w.height = lines.size(); w.width = max_len; // allocate 2d array for keeping track of things, height x width, pointers to Object w.grid = new Object**[w.height]; for(int y=0; y < w.height; y++) { w.grid[y] = new Object*[w.width]; for(int x=0; x < w.width; x++) w.grid[y][x] = NULL; } // scan through the lines, creating objects and putting into world // will create Object's and put both into objs linked list and 2d grid for(unsigned i=0; i < lines.size(); i++) { for(unsigned j=0; j < lines[i].length(); j++) { char ch = lines[i][j]; // for this character read from the world file, create an object for anything // that is not a space or newline if (ch != ' ' && ch != '\n') { // all objects have a char, x, and y Object * ob = new Object(ch); ob->x = j; ob->y = lines.size()-1 - i; // last line is height 0, first line is lines.size()-1 // some objects have other parameters to set switch(ch) { case '@': ob->vx = 3; if (rand() % 2) ob->vx *= -1; // will be left or right ob->moves = true; break; case '&': ob->num = 4; // 4 coins break; } // put object in our objs list and our 2d grid w.objs.push_back(ob); w.grid[(int) ob->y][(int) ob->x] = ob; // save pointer in the grid as well } } } // success return true; } // main game loop - draw, get key, handle key, update world. repeat void Game::play_game() { int chTyped; do { s.draw(); chTyped = getch(); usleep(w.delta_s * 1000000); if (! gameOver) { handle_key(chTyped); w.update(); } } while (chTyped != 'q'); } // take key user typed and do whatever we should do void Game::handle_key(int ch) { // player obj is always first in the linked list Player *p = (Player *) (*(w.objs.begin())); // to keep track if we moved the player int new_x = -1; switch(ch) { case KEY_UP: // start jump by setting y velocity, if not already in middle of jump if (! p->isMoving_y) { p->vy = 25; // #rows up / sec p->isMoving_y = true; } break; case KEY_DOWN: // don't do anything, for now break; case KEY_LEFT: // move one to left if not on edge of screen new_x = p->x > 0 ? p->x-1 : 0; moveObject(p, p->y, new_x); break; case KEY_RIGHT: // move one to right if not on edge of screen new_x = p->x < s.max_x ? p->x+1 : s.max_x; moveObject(p, p->y, new_x); break; } } void Game::moveObject(Object *obj, double new_y, double new_x) { if (obj == NULL) return; // don't move if would be a barrier if (s.barrier(new_y, new_x)) return; // if x changed, check for falling y if ((int) obj->x != (int) new_x) { if (!s.barrier(new_y-1, new_x)) obj->isMoving_y = true; } // check for collision between obj and something there already Object *other_obj = w.grid[(int) new_y][(int) new_x]; Player *p = (Player *) *(w.objs.begin()); if (other_obj != NULL && other_obj != obj && ! obj->flagToRemove && // only take one coin or life, in case we flagged already ! other_obj->flagToRemove && (p == obj || other_obj == p)) { // something there that is not the player, deal with it char c = p == obj ? other_obj->ch : obj->ch; switch (c) { case '@': // if it was player moving and jumped on it, then don't lose life if (p == obj && (int)new_y == (int)p->y -1) ; else { p->loseLife(); if (p->getLives() <= 0) gameOver = true; } break; case '$': p->coins += 1; break; } // and flag it for removal if (p != obj) obj->flagToRemove = true; else if (p != other_obj) other_obj->flagToRemove = true; removedCount++; } // now, actually move it w.grid[(int) obj->y][(int) obj->x] = NULL; // not where it was anymore obj->y = new_y; obj->x = new_x; w.grid[(int) obj->y][(int) obj->x] = obj; } bool Game::isPlayer(Object *obj) { if (obj == *(w.objs.begin())) return true; else return false; } #endif