/*
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