Project 3
For questions about this project, first consult your TA.
If your TA can’t help, ask Professor Nachenberg.
Time due:
Part 1: Saturday, February 24
Part 2: Sunday, March 3
WHEN IN DOUBT ABOUT A REQUIREMENT, YOU WILL NEVER LOSE CREDIT
IF YOUR SOLUTION WORKS THE SAME AS OUR POSTED SOLUTION.
SO PLEASE DO NOT ASK ABOUT ITEMS WHERE YOU CAN DETERMINE THE
PROPER BEHAVIOR ON YOUR OWN FROM OUR SOLUTION!
PLEASE THROTTLE THE RATE YOU ASK QUESTIONS
TO 1 EMAIL PER DAY! IF YOU’RE SOMEONE WITH
LOTS OF QUESTIONS, SAVE THEM UP AND ASK ONCE.
2
Table of Contents
Introduction......................................................................................................................... 4
Game Details....................................................................................................................... 5
So how does a video game work?....................................................................................... 8
What Do You Have to Do?............................................................................................... 11
You Have to Create the StudentWorld Class................................................................ 11
init() Details .............................................................................................................. 14
move() Details........................................................................................................... 15
Give Each Actor a Chance to Do Something........................................................ 17
Remove Dead Actors after Each Tick .................................................................. 18
Updating the Display Text.................................................................................... 18
cleanUp() Details ...................................................................................................... 19
The Level Class and Level Data File ............................................................................ 19
The Level Class......................................................................................................... 21
You Have to Create the Classes for All Actors ............................................................ 22
The player ................................................................................................................. 25
What the Avatar Must Do When It Is Created...................................................... 25
What the Avatar Must Do During a Tick.............................................................. 26
What the player Must Do When It Is Attacked..................................................... 27
Getting Input From the User................................................................................. 27
Wall........................................................................................................................... 27
What a Wall Must Do When It Is Created............................................................ 28
What a Wall Must Do During a Tick.................................................................... 28
What a Wall Must Do When It Is Attacked.......................................................... 28
Marble ....................................................................................................................... 28
What a Marble Must Do When It Is Created ........................................................ 28
What a Marble Must Do During a Tick ................................................................ 29
What a Marble Must Do When It Is Attacked ...................................................... 29
What a Marble Must Do When It Is Pushed ......................................................... 29
Pea............................................................................................................................. 30
What a Pea Must Do When It Is Created.............................................................. 30
What a Pea Must Do During a Tick...................................................................... 30
What a Pea Must Do When It Is Attacked............................................................ 31
Pit.............................................................................................................................. 31
What a Pit Must Do When It Is Created ............................................................... 31
What a Pit Must Do During a Tick ....................................................................... 31
What a Pit Must Do When It Is Attacked ............................................................. 32
Crystal....................................................................................................................... 32
What a Crystal Must Do When It Is Created ........................................................ 32
What a Crystal Must Do During a Tick ................................................................ 32
What a Crystal Must Do When It Is Attacked ...................................................... 33
The Exit..................................................................................................................... 33
What the Exit Must Do When It Is Created.......................................................... 33
What the Exit Must Do During a Tick.................................................................. 33
3
What the Exit Must Do When It Is Attacked........................................................ 33
What the Exit Must Do When It Is Revealed ....................................................... 34
Extra Life Goodie ..................................................................................................... 34
What an Extra Life Goodie Must Do When It Is Created..................................... 34
What an Extra Life Goodie Must Do During a Tick ............................................ 34
What an Extra Life Goodie Must Do When It Is Attacked................................... 35
Restore Health Goodie.............................................................................................. 35
What a Restore Health Goodie Must Do When It Is Created ............................... 35
What a Restore Health Goodie Must Do During a Tick ....................................... 35
What a Restore Health Goodie Must Do When It Is Attacked ............................. 36
Ammo Goodie........................................................................................................... 36
What an Ammo Goodie Must Do When It Is Created.......................................... 36
What an Ammo Goodie Must Do During a Tick.................................................. 36
What an Ammo Goodie Must Do When It Is Attacked........................................ 37
RageBot..................................................................................................................... 37
What a RageBot Must Do When It Is Created...................................................... 37
What a RageBot Must Do During a Tick.............................................................. 37
What a RageBot Must Do When It Is Attacked.................................................... 38
ThiefBot .................................................................................................................... 38
What a ThiefBot Must Do When It Is Created ..................................................... 39
What a ThiefBot Must Do During a Tick ............................................................. 39
What a ThiefBot Must Do When It Is Attacked ................................................... 40
Mean ThiefBot.......................................................................................................... 41
What a Mean ThiefBot Must Do When It Is Created ........................................... 41
What a Mean ThiefBot Must Do During a Tick ................................................... 41
What a Mean ThiefBot Must Do When It Is Attacked ......................................... 43
ThiefBot Factory....................................................................................................... 43
What a ThiefBot Factory Must Do When It Is Created ........................................ 43
What a ThiefBot Factory Must Do During a Tick................................................ 44
What a ThiefBot Factory Must Do When It Is Attacked ...................................... 44
Object Oriented Programming Best Practices .................................................................. 44
Don’t know how or where to start? Read this! ................................................................. 49
Building the Game ............................................................................................................ 50
For Windows................................................................................................................. 50
For macOS .................................................................................................................... 51
What to Turn In................................................................................................................. 51
Part #1 (20%)................................................................................................................ 51
What to Turn In For Part #1.......................................................................................... 53
Part #2 (80%)................................................................................................................ 54
What to Turn In For Part #2.......................................................................................... 54
FAQ................................................................................................................................... 55
4
Introduction
NachenGames corporate spies have learned that SmallSoft is planning to release a new
game called Marble Madness, and would like you to program an exact copy so
NachenGames can beat SmallSoft to the market. To help you, NachenGames corporate
spies have managed to steal a prototype Marble Madness executable file and several
source files from the SmallSoft headquarters, so you can see exactly how your version of
the game must work (see posted executable file) and even get a head start on the
programming. Of course, such behavior would never be appropriate in real life, but for
this project, you’ll be a programming villain.
In Marble Madness, the player has to navigate through a series of robot-infested mazes in
order to gather valuable crystals. After the player has gathered each of the crystals within
a particular maze, an exit will be revealed, and the player may then use the exit to
advance to the next maze. The player wins by completing all of the mazes.
Here is an example of what the Marble Madness game looks like:
Figure #1: A screenshot of the Marble Madness game. You can see the player (top-middle), two different
types of robots (RageBots and ThiefBots), a bunch of marbles, a bunch of pits (that can only be crossed if
they are filled in with a marble), ThiefBot factories, blue crystals, a healing kit, and an ammo kit.
5
Game Details
In Marble Madness, the player starts out a new game with three lives and continues to
play until all of his/her lives have been exhausted. There are multiple levels in Marble
Madness, beginning with level 0, and each level has its own maze. During each level, the
player must gather all of the blue crystals within the current maze before the exit is
revealed and they may use it to move on to the next level.
Upon starting each level, the player’s avatar is placed in a maze filled with one or more
blue crystals, marbles, pits, robots, robot factories and other goodies. The player may use
the arrow keys to move their avatar (the Indiana Jones-looking character) left, right, up
and down through the maze. They may walk on any square so long as it doesn’t have a
wall, a marble, a factory, a robot, or a pit on it. The player may walk onto a square
containing a marble if they are able to push it out of the way first. In addition to walking
around the maze, the player may also shoot their pea cannon – but beware, it has only
limited peas. Peas can destroy robots as well as marbles, but it takes more than one shot
to do so.
There are three different types of goodies distributed throughout the maze that the player
can collect in addition to blue crystals. If the player’s avatar steps upon the same square
as an extra life goodie, it instantly gives the player an extra life. If the avatar steps onto
the same square as a restore health goodie, it will restore the player to full health (in case
of having been injured by shots from the robots). Finally, if the avatar steps onto the same
square as an ammo goodie, it will give the player 20 additional peas.
There are four major types of robots in Marble Madness: Horizontal RageBots, Vertical
RageBots, Regular ThiefBots and Mean ThiefBots
As mentioned, RageBots fall into two categories – Horizontal RageBots and Vertical
RageBots. Horizonal RageBots simply move back and forth on a row of the screen (only
reversing course when they run into an obstacle), shooting at the player’s avatar if he/she
ever walks in their line of sight. Vertical RageBots are identical to their Horizontal
cousins, but move up and down within a single column of the maze. RageBots can start
out anywhere in the maze (depending on where the designer of the maze choses to put
them).
In contrast to the RageBot, the ThiefBots are a bit nastier. These robots wander around
the maze looking for goodies to steal. If they happen to step onto a goodie (an extra life
goodie, a restore health goodie, or an ammo goodie) and they don’t already hold one,
they may pick it up for themselves. As with RageBots, there are two types of ThiefBots:
Regular ThiefBots and Mean ThiefBots. Regular ThiefBots simply wander aimlessly
around the maze looking for, and picking up, goodies. They are otherwise harmless and
will not fire upon the player’s avatar. In contrast, in addition to picking up goodies, Mean
ThiefBots will fire a pea anytime the player steps in their path. So beware! When any
type of ThiefBot dies, if it previously picked up a goodie, it will drop this object upon its
square in the maze.
6
ThiefBots are created by ThiefBot factories, of which there are two types – one that
produces Regular ThiefBots and one that produces Mean ThiefBots. ThiefBots never
start out in the maze; they are added only by factories.
Once the player has collected all of the blue crystals within the current maze, an exit will
appear. The exit is invisible and unusable until all of the crystals have been collected
from the level. Once the exit has been revealed, the player must direct their avatar to the
exit in order to advance to the next level. The player will be granted 2000 points for
exiting a level. The player will also be given a bonus for completing the level quickly, if
they did so fast enough. The game is complete once the player has used the exit on the
last level.
If the player’s health reaches zero (the player loses health when shot), their avatar dies
and loses one “life.” If, after losing a life, the player has one or more remaining lives left,
they are placed back on the current level and they must again solve the entire level from
scratch (with the level starting as it was at the beginning of the first time it was
attempted). The player will restart the level with full health points, as well as 20 peas
(regardless of how many they had when they died). If the avatar dies and has no lives left,
then the game is over.
The Marble Madness maze is exactly 15 squares wide by 15 squares high, and both the
player’s avatar and robots may move to any adjacent square that doesn’t contain a wall, a
pit, a marble, a robot, the player or a factory. The one exception is that ThiefBots are
born in Factories, so they may start out on the same square as a factory, but are not
allowed to move back onto their birth factory once they’ve left it. The bottom-leftmost
square has coordinates x=0,y=0, while the upper-rightmost square has coordinate
x=14,y=14, where x increases to the right and y increases upward toward the top of the
screen. You can look in our provided file, GameConstants.h, for constants that represent
the maze’s width and height (VIEW_WIDTH and VIEW_HEIGHT).
In Marble Madness, each level’s maze is stored in a different data file. For example, the
first level’s maze is stored in a file called level00.txt. The second level’s maze is stored
in a file called level01.txt, and so on. Each time the player is about to start a new level,
your code must load the data from the appropriate data file (using a class called Level that
we provide) and then use this data to determine the layout of the current level.
Each level data file contains a specification for the layout of the maze, the initial
locations of all the RageBots and the player’s avatar, as well as the initial locations of all
marbles, pits, factories, crystals, goodies and the exit. For more information on the level
data files, please see the Level Data File section below. You may define your own level
data files to customize your game (and more importantly, to test it).
Once a new maze has been prepared and the player’s avatar and all the robots and items
in the maze have been properly situated, the game play begins. Game play is divided into
7
ticks, and there are twenty ticks per second (to provide smooth animation and game play).
During each tick, the following occurs:
1. The player has an opportunity to move their avatar exactly one square
horizontally or vertically, fire their pea cannon (if they have peas), or give up
(some levels are unsolvable if the player makes a mistake, so if the player realizes
this, they can press the Escape key to lose a life and restart the level from scratch).
2. Every other object in the maze (e.g., RageBots, ThiefBots, factories, goodies, etc.)
is given an opportunity to do something. For example, when given the opportunity
to do something, a RageBot can move one square (left, right, up or down)
according to its built-in movement algorithm (the RageBot movement algorithms
are described in detail in the various RageBot sections below).
The player controls the direction of their avatar with the arrow keys, or for lefties and
others for whom the arrow key placement is awkward, WASD or the numeric keypad: up
is w or 8, left is a or 4, down is s or 2, right is d or 6. The player may move their avatar
between the maze’s Walls as they please. The player can sacrifice one life and restart the
current level by pressing the Escape key at any time.
The player’s avatar may fire their pea cannon by pressing the space bar. A pea that hits a
RageBot or a ThiefBot does 2 points of damage to it. If, after repeated hits, the robot
dies, the player earns points:
For destroying a RageBot of any type: 100 points
For destroying a Regular ThiefBot: 10 points
For destroying a Mean ThiefBot: 20 points
The player also earns points (and special benefits) by picking up (i.e., moving onto the
same square as) various items:
Blue Crystal: 50 points (all crystals must be collected to advance to the next level)
Extra Life Goodie: 1000 points (the user gets an extra life)
Restore Health Goodie: 500 points (the user’s health is restored to 100%)
Ammo Goodie: 100 points (the user receives 20 additional peas)
Players also earn bonus points for completing a level quickly. Each level’s maze starts
with a bonus score of 1000 points. During each tick of the game, the bonus score is
reduced by one point until the bonus reaches zero (the bonus never goes below zero). If
and when the player completes the current level, whatever bonus remains is added onto
their score. This incentivizes the player to complete each level as quickly as possible
since the player wants to maximize their score in the game.
The player starts with three lives. The player loses a life if their health reaches zero (from
being shot by robots).
8
When a Player dies, the player’s number of remaining lives is decremented by 1. If the
player still has at least one life left, then the user is prompted to continue and given
another chance by restarting the current maze level from scratch. All the RageBots that
were initially on the level will again be alive and returned to their starting positions, the
player’s avatar will be returned to their starting position, and the maze will revert back to
its original state (all crystals, goodies, pits and marbles in their initial positions). In
addition, if the exit was exposed in the maze prior to the avatar’s death, then this too will
be hidden from the maze until such time that the player collects all Crystals on the level.
Then game play restarts. If the player is killed and has no lives left, then the game is over.
Pressing the q key lets you quit the game prematurely.
So how does a video game work?
Fundamentally, a video game is composed of a bunch of objects; in Marble Madness,
those objects include the player’s avatar, RageBots (Horizontal and Vertical), ThiefBots
(Regular and Mean), goodies (e.g., extra life goodies), crystals, marbles, pits, walls,
factories, and the exit. Let’s call these objects “actors,” since each object is an actor in
our video game. Each actor has its own x,y location in the maze, its own internal state
(e.g., a RageBot knows its location, what direction it’s moving, etc.) and its own special
algorithms that control its actions in the game based on its own state and the state of the
other objects in the world. In the case of the player’s avatar, the algorithm that controls
the avatar actor object is the user’s own brain and hand, and the keyboard! In the case of
other actors (e.g., RageBots), each object has an internal autonomous algorithm and state
that dictates how the object behaves in the game world.
Once a game begins, gameplay is divided into ticks. A tick is a unit of time, for example,
50 milliseconds (that’s 20 ticks per second).
During a given tick, the game calls upon each object’s behavioral algorithm and asks the
object to perform its behavior. When asked to perform its behavior, each object’s
behavioral algorithm must decide what to do and then make a change to the object’s state
(e.g., move the object 1 square to the left), or change other objects’ states (e.g., when a
RageBot’s algorithm is called by the game, it may determine that the player’s avatar has
moved into its line of fire, and it may fire its pea cannon). Typically, the behavior
exhibited by an object during a single tick is limited in order to ensure that the gameplay
is smooth and that things don’t move too quickly and confuse the player. For example, a
RageBot will move just one square left/right/up/down, rather than moving two or more
squares; a RageBot moving, say, 5 squares in a single tick would confuse the user,
because humans are used to seeing smooth movement in video games, not jerky shifts.
After the current tick is over and all actors have had a chance to adjust their state (and
possibly adjust other actors’ states), our game framework (that we provide) animates the
actors onto the screen in their new configuration. So, if a RageBot changed its location
from 10,5 to 11,5 (moved one square right), then our game framework would erase the
graphic of the RageBot from location 10,5 on the screen and draw the RageBot’s graphic
9
at 11,5 instead. Since this process (asking actors to do something, then animating them to
the screen) happens 20 times per second, the user will see somewhat smooth animation.
Then, the next tick occurs, and each object’s algorithm is again allowed to do something,
our framework displays the updated actors on-screen, etc.
Assuming the ticks are quick enough (a fraction of a second), and the actions performed
by the objects are subtle enough (i.e., a RageBot doesn’t move 3 inches away from where
it was during the last tick, but instead moves 1 millimeter away), when you display each
of the objects on the screen after each tick, it looks as if each object is performing a
continuous series of fluid motions.
A video game can be broken into three different phases:
Initialization: The game World is initialized and prepared for play. This involves
allocating one or more actors (which are C++ objects) and placing them in the game
world so that they will appear in the maze.
Game play: Game play is broken down into a bunch of ticks. During each tick, all of the
actors in the game have a chance to do something, and perhaps die. During a tick, new
actors may be added to the game and actors who die must be removed from the game
world and deleted.
Cleanup: The player has lost a life (but has more lives left), the player has completed the
current level, or the player has lost all of their lives and the game is over. This phase frees
all of the objects in the World (e.g., robots, walls, marbles, goodies, the player’s avatar,
etc.) since the level has ended. If the game is not over (i.e., the player has more lives),
then the game proceeds back to the Initialization step, where the maze is repopulated
with new occupants and game play restarts at the current level.
Here is what the main logic of a video game looks like, in pseudocode (we provide some
similar code for you in our provided GameController.cpp):
while (The player has lives left)
{
Prompt_the_user_to_start_playing(); // "press a key to start"
Initialize_the_game_world(); // you’re going to write this
while (the player is still alive)
{
// each pass through this loop is a tick (1/20th of a sec)
// you’re going to write code to do the following
Ask_all_actors_to_do_something();
Delete_any_dead_actors_from_the_world();
// we write this code to handle the animation for you
Animate_all_of_the_actors_to_the_screen();
Sleep_for_50ms_to_give_the_user_time_to_react();
}
10
// the player died – you’re going to write this code
Cleanup_all_game_world_objects(); // you’re going to write this
if (the player is still alive)
Prompt_the_Player_to_continue();
}
Tell_the_user_the_game_is_over(); // we provide this
And here is what the Ask_all_actors_to_do_something() function might look like:
void Ask_all_actors_to_do_something()
{
for each actor on the level:
if (the actor is still alive)
tell the actor to doSomething();
}
You will typically use a container (an array, vector, or list) to hold pointers to each of
your live actors. Each actor (a C++ object) has a doSomething( ) member function in
which the actor decides what to do. For example, here is some pseudocode showing what
a (simplified) RageBot might decide to do each time it gets asked to do something:
class RageBot: public SomeOtherClass
{
public:
void doSomething()
{
If the player is in my line of sight, then
Fire my pea cannon in the direction of the player
Else if I can move in my current direction w/o hitting an obstacle, then
Move one square in my current direction
Else if I’m about to run into an obstacle, then
Reverse my direction, but don’t move during this tick
}
...
};
And here’s what the player’s doSomething( ) member function might look like:
class Player: public …
{
public:
void doSomething()
{
Try to get user input (if any is available)
If the user pressed the UP key and that square is open then
Increase my y location by one
If the user pressed the DOWN key and that square is open then
Decrease my y location by one
...
If the user pressed the space bar to fire and the player has
peas, then
Introduce a new pea object into the game
...
}
...
};
11
What Do You Have to Do?
You must create a number of different classes to implement the Marble Madness game.
Your classes must work properly with our provided classes, and you must not modify
our classes or our source files in any way to get your classes to work properly (doing
so will result in a score of zero on the entire project!). Here are the specific classes that
you must create:
1. You must create a class called StudentWorld which is responsible for keeping
track of your game world (including the maze) and all of the actors/objects
(RageBots, ThiefBots, peas, crystals, goodies, pits, marbles, the player’s avatar,
etc.) that are inside the maze.
2. You must create a class to represent the player in the game.
3. You must create classes for Horizontal/Vertical RageBots, Regular ThiefBots,
Mean ThiefBots, factories, marbles, pits, walls, crystals, extra life goodies, restore
health goodies, ammo Goodies, walls, and the exit, as well as any additional base
classes (e.g., a robot base class if you find it convenient) that help you implement
the game.
You Have to Create the StudentWorld Class
Your StudentWorld class is responsible for orchestrating virtually all game play – it keeps
track of the game world (the maze and all of its inhabitants such as RageBots, ThiefBots,
the player, marbles, walls, the exit, goodies, etc.). It is responsible for initializing the
game world at the start of the game, asking all the actors to do something during each tick
of the game, destroying an actor when it disappears (e.g., a RageBot dies), and destroying
all of the actors in the game world when the user loses a life.
Your StudentWorld class must be derived from our GameWorld class (found in
GameWorld.h) and must implement at least these three methods (which are defined as
pure virtual in our GameWorld class):
virtual int init() = 0;
virtual int move() = 0;
virtual void cleanUp() = 0;
The code that you write must never call any of these three functions. Instead, our
provided game framework will call these functions for you. So, you have to implement
them correctly, but you won’t ever call them yourself in your code.
When a new level starts (e.g., at the start of a game, or when the player completes a level
and advances to the next level), our game framework will call the init() method that you
12
defined in your StudentWorld class. You don’t call this function; instead, our provided
framework code calls it for you.
The init() method is responsible for loading the current level’s maze from a data file
(we’ll show you how below), and constructing a representation of the current level in
your StudentWorld object, using one or more data structures that you come up with.
The init() method is automatically called by our provided code either (a) when the game
first starts, (b) when the player completes the current level and advances to a new level
(that needs to be loaded/initialized), or (c) when the user loses a life (but has more lives
left) and the game is ready to restart at the current level.
When the player has finished the level loaded from level00.txt, the next level data file to
load is level01.txt; after level01.txt, level02.txt; etc. If there is no level data file with the
next number, or if the level just completed is level 99, the init() method must return
GWSTATUS_PLAYER_WON. If the next level file exists but is not in the proper format for a level
data file, the init() method must return GWSTATUS_LEVEL_ERROR. Otherwise, the init()
method must return GWSTATUS_CONTINUE_GAME.
Once a new level has been loaded/initialized with a call to the init() method, our game
framework will repeatedly call the StudentWorld’s move() method, at a rate of roughly 20
times per second. Each time the move() method is called, it must run a single tick of the
game. This means that it is responsible for asking each of the game actors (e.g., the
player’s avatar, each RageBot, goodie, etc.) to try to do something: e.g., move themselves
and/or perform their specified behavior. Finally, this method is responsible for disposing
of (i.e., deleting) actors (e.g., a pea, a dead RageBot, etc.) that need to disappear during a
given tick. For example, if a RageBot is shot by the player and its “hit points” (life force)
drains to zero, then its state should be set to dead, and then after all of the actors in the
game get a chance to do something during the tick, the move() method should remove
that RageBot from the game world (by deleting its object and removing any reference to
the object from the StudentWorld’s data structures). The move() method will
automatically be called once during each tick of the game by our provided game
framework. You will never call the move() method yourself.
The cleanup() method is called by our framework when the player completes the current
level or loses a life (i.e., her/his hit points reach zero due to being shot by the robots). The
cleanup() method is responsible for freeing all actors (e.g., all RageBot objects, all
ThiefBot objects, all wall objects, the Avatar object, the exit object, all goodie objects,
crystal objects, all pea objects, etc.) that are currently in the game. This includes all actors
created during either the init() method or introduced during subsequent game play by the
actors in the game (e.g., a ThiefBot that was added to the maze by a ThiefBot factory)
that have not yet been removed from the game.
You may add as many other public/private member functions or private data members to
your StudentWorld class as you like (in addition to the above three member functions,
which you must implement).
13
Your StudentWorld class must be derived from our GameWorld class. Our GameWorld
class provides the following methods for your use:
unsigned int getLevel() const;
unsigned int getLives() const;
void decLives();
void incLives();
unsigned int getScore() const;
void increaseScore(unsigned int howMuch);
void setGameStatText(string text);
string assetPath() const;
bool getKey(int& value);
void playSound(int soundID);
getLevel() can be used to determine the current level number.
getLives() can be used to determine how many lives the player has left.
decLives() reduces the number of Player lives by one.
incLives() increases the number of Player lives by one.
getScore() can be used to determine the player’s current score.
increaseScore() is used by a StudentWorld object (or your other classes) to increase the
user’s score upon successfully destroying a robot, picking up a goodie of some sort, or
completing a level (to give the player their remaining level bonus). When your code calls
this method, you must specify how many points the user gets (e.g., 100 points for
destroying a RageBot). This means that the game score is controlled by our GameWorld
object – you must not maintain your own score data member in your own classes.
The setGameStatText() method is used to specify what text is displayed at the top of the
game screen, e.g.:
Score: 0321000 Level: 05 Lives: 3 Health: 70% Ammo: 20 Bonus: 742
assetPath() returns the path to the directory that contains the game assets (image, sound,
and level data files).
getKey() can be used to determine if the user has hit a key on the keyboard to move the
player, to fire, or to sacrifice one life and restart the level. This method returns true if the
user hit a key during the current tick, and false otherwise (if the user did not hit any key
during this tick). The only argument to this method is a variable that will be set to the key
that was pressed by the user (if any key was pressed). If the function returns true, the
argument will be set to one of the following values (defined in GameConstants.h):
KEY_PRESS_LEFT
KEY_PRESS_RIGHT
14
KEY_PRESS_UP
KEY_PRESS_DOWN
KEY_PRESS_SPACE
KEY_PRESS_ESCAPE
The playSound() method can be used to play a sound effect when an important event
happens during the game (e.g., a robot dies or the player picks up a crystal). You can
find constants (e.g., SOUND_ROBOT_DIE) that describe what noise to make in the
GameConstants.h file. Here’s how this method might be used:
// if a RageBot reaches zero hit points and dies, make a dying sound
if (theRobotHasZeroHitPoints())
studentWorldObject->playSound(SOUND_ROBOT_DIE);
init() Details
Your StudentWorld’s init() member function must:
1. Initialize the data structures used to keep track of your game’s world.
2. Load the current maze details from a level data file.
3. Allocate and insert a valid Avatar object into the game world.
4. Allocate and insert any RageBots objects, wall objects, marble objects, factory
objects, crystal objects, goodie objects, or exit objects into the game world, as
required by the specification in the current level’s data file.
To load the details of the current level from a level data file, you can the Level class
(described later) that we wrote for you, which can be found in the provided Level.h
header file. Here’s a brief example that uses the Level class to load a level data file:
#include "Level.h" // you must include this file to use our Level class
int StudentWorld::someFunctionYouWriteToLoadALevel()
{
string curLevel = "level03.txt";
Level lev(assetPath());
Level::LoadResult result = lev.loadLevel(curLevel);
if (result == Level::load_fail_file_not_found ||
result == Level:: load_fail_bad_format)
return -1; // something bad happened!
// otherwise the load was successful and you can access the
// contents of the level – here’s an example
int x = 0;
int y = 5;
Level::MazeEntry item = lev.getContentsOf(x, y);
if (item == Level::player)
cout << "The player should be placed at 0,5 in the maze\n";
x = 10;
y = 7;
15
item = lev.getContentsOf(x, y);
if (item == Level::wall)
cout << "There should be a wall at 10,7 in the maze\n":
… // etc
}
Notice that the getContentsOf() method takes the column parameter (x) first, then the row
parameter (y) second. This is different than the order one normally uses when indexing a
2-dimensional array, which would be array[row][col]. Be careful!
You can examine the Level.h file for a full list of functions that you can use to access
each level.
Once you load a level’s layout and details using our Level class, your init() method must
then construct a representation of your world and store this in a StudentWorld object. It
is required that you keep track of all of the actors (e.g., RageBots, walls, extra life
goodies, the exit, crystals, marbles, etc.) in a single STL collection like a list or vector.
(To do so, we recommend using a container of pointers to the actors). If you like, your
StudentWorld object may keep a separate pointer to the Avatar object rather than keeping
a pointer to that object in the container with the other actor pointers; the player is the only
actor allowed to not be stored in the single actor container.
You must not call the init() method yourself. Instead, this method will be called by our
framework code when it’s time for a new game to start (or when the player completes a
level or needs to restart a level).
move() Details
The move() method must perform the following activities:
1. It must ask all of the actors that are currently active in the game world to do
something (e.g., ask a RageBot to move itself, ask a factory to potentially produce
a new ThiefBot, give the player a chance to move up, down, left or right, etc.).
a. If an actor does something that causes the player to die, then the move()
method should immediately return GWSTATUS_PLAYER_DIED.
b. If the player steps onto the same square as an exit (after first collecting all
of the crystals on the level), completing the current level, then the move()
method should immediately:
i. Increase the player’s score appropriately (by 2000 points for using
the exit, and by the remaining bonus score for the level).
ii. Return a value of GWSTATUS_FINISHED_LEVEL.
2. It must then delete any actors that have died during this tick (e.g., a RageBot that
was killed by a pea so both should be removed from the game world, or a goodie
that disappeared because the player picked it up).
3. It must reduce the level’s bonus points by one point during each tick. Each level
starts with a bonus of 1000, then goes down during each tick. So, after the first
16
tick, the bonus would drop to 999, after the second tick to 998, etc. This declining
bonus value incentivizes the player to complete the level as quickly as possible to
get the biggest bonus score. The level bonus may not go below a value of zero.
4. It should expose/activate the exit on the current level once the player has collected
all of the crystals on the level (alternatively, the exit object can do this instead of
the StudentWorld object). This enables the player to later move over to the exit
and complete the level, advancing to the next level.
5. It must update the status text on the top of the screen with the latest information
(e.g., the user’s current score, the remaining bonus score for the level, etc.).
The move() method must return one of three different values when it returns at the end of
each tick (all are defined in GameConstants.h):
GWSTATUS_PLAYER_DIED
GWSTATUS_CONTINUE_GAME
GWSTATUS_FINISHED_LEVEL
The first return value indicates that the player died during the current tick, and instructs
our provided framework code to tell the user the bad news and restart the level if the user
has more lives left. If your move() method returns this value and the player has more
lives left, then our framework will prompt the player to continue the game, call your
cleanup() method to destroy the level, call your init() method to re-initialize the level
from scratch, and then begin calling your move() method over and over, once per tick, to
let the user play the level again.
The second return value indicates that the tick completed without the player dying BUT
the player has not yet completed the current level. Therefore, the game play should
continue normally for the time being. In this case, the framework will advance to the
next tick and call your move() method again.
The final return value indicates that the player has completed the current level (that is,
gathered all of the crystals on the level and stepped onto the same square as the exit). If
your move() method returns this value, then the current level is over, and our framework
will call your cleanup() method to destroy the level, advance to the next level, then call
your init() method to prepare that level for play, etc…
Here’s pseudocode for how the move() method might be implemented:
int StudentWorld::move()
{
// Update the Game Status Line
updateDisplayText(); // update the score/lives/level text at screen top
// The term "actors" refers to all robots, the player, Goodies,
// Marbles, Crystals, Pits, Peas, the exit, etc.
// Give each actor a chance to do something
for each of the actors in the game world
{
if (actor[i] is still active/alive)
17
{
// ask each actor to do something (e.g. move)
actor[i]->doSomething();
if (thePlayerDiedDuringThisTick())
return GWSTATUS_PLAYER_DIED;
if (thePlayerCompletedTheCurrentLevel())
{
increaseScoreAppropriately();
return GWSTATUS_FINISHED_LEVEL;
}
}
}
// Remove newly-dead actors after each tick
removeDeadGameObjects(); // delete dead game objects
// Reduce the current bonus for the Level by one
reduceLevelBonusByOne();
// If the player has collected all of the crystals on the level, then we
// must expose the exit so the player can advance to the next level
if (thePlayerHasCollectedAllOfTheCrystalsOnTheLevel())
exposeTheExitInTheMaze(); // make the exit Active
// return the proper result
if (thePlayerDiedDuringThisTick())
return GWSTATUS_PLAYER_DIED;
if (thePlayerCompletedTheCurrentLevel())
{
increaseScoreAppropriately();
return GWSTATUS_FINISHED_LEVEL;
}
// the player hasn’t completed the current level and hasn’t died, so
// continue playing the current level
return GWSTATUS_CONTINUE_GAME;
}
Give Each Actor a Chance to Do Something
During each tick of the game each active actor must have an opportunity to do something
(e.g., move around, shoot, etc.). Actors include the player’s avatar, RageBots, ThiefBots,
walls, crystals, marbles, factories, goodies, pits, peas, and the exit.
Your move() method must enumerate each active actor in the maze (i.e., held by your
StudentWorld object) and ask it to do something by calling a member function in the
actor’s object named doSomething(). In each actor’s doSomething() method, the object
will have a chance to perform some activity based on the nature of the actor and its
current state: e.g., a RageBot might move one step forward, the player might shoot a pea,
a marble may fill a pit in the maze, etc.
It is possible that one actor (e.g., a pea) may destroy another actor (e.g., a RageBot)
during the current tick. If an actor has died earlier in the current tick, then the dead actor
must not have a chance to do something during the current tick (since it’s dead).
18
To help you with testing, if you press the f key during the course of the game, our game
controller will stop calling move() every tick; it will call move() only when you hit a key
(except the r key). Freezing the activity this way gives you time to examine the screen,
and stepping one move at a time when you're ready helps you see if your actors are
moving properly. To resume regular game play, press the r key.
Remove Dead Actors after Each Tick
At the end of each tick, your move() method must determine which of your actors are no
longer alive, remove them from your container of active actors, and delete their objects
(so you don’t have a memory leak). So if, for example, a RageBot’s hit points go to zero
(due to it being shot) and it dies, then it should be noted as dead, and at the end of the
tick, its pointer should be removed from the StudentWorld’s container of active objects,
and the RageBot object should be deleted (using the C++ delete expression) to free up
memory for future actors that will be introduced later in the game. (Hint: Each of your
actors could have a data member indicating whether or not it is still alive)
Updating the Display Text
Your move() method must update the game statistics at the top of the screen during every
tick by calling the setGameStatText() method that we provide in our GameWorld class.
You could do this by calling a function like the one below from the move() method:
void setDisplayText()
{
int score = getCurrentScore();
int level = getCurrentGameLevel();
unsigned int bonus = getCurrentLevelBonus();
int livesLeft = getNumberOfLivesThePlayerHasLeft();
// Next, create a string from your statistics, of the form:
// Score: 0000100 Level: 03 Lives: 3 Health: 70% Ammo: 216 Bonus: 34
string s = someFunctionToFormatThingsNicely(score, level, lives,
health, ammo, bonus);
// Finally, update the display text at the top of the screen with your
// newly created stats
setGameStatText(s); // calls our provided GameWorld::setGameStatText
}
Your status line must meet the following requirements:
1. Each field’s label is followed by a colon and one space.
2. Each field’s value after the colon and space must be exactly as wide as shown in
the example above:
a. The Score field must be 7 digits long, with leading zeros.
b. The Level field must be 2 digits long, with leading zeroes.
c. The Lives field must be 2 digits long, with leading spaces (e.g., “_ 2”,
where _ in this sentence represents a space).
19
d. The Health field must be 3 digits long and display the player’s health
percentage (not hit points!), with leading spaces, and be followed by a
percent sign (e.g., “_70%”).
e. The Ammo field should be 3 digits long, with leading spaces (e.g., “_
24”).
f. The Bonus field must be 4 digits long, with leading spaces (e.g., “_924”).
3. Each statistic must be separated from the previous statistic by two spaces. For
example, between the “0000100” of the score and the “L” in “Level” there must
be exactly two spaces.
You may find the Stringstreams writeup on the class web site to be helpful.
cleanUp() Details
When your cleanUp() method is called by our game framework, it means that the player
lost a life (e.g., their hit points reached zero due to being shot) or has completed the
current level. In this case, every actor in the entire maze (the player and every RageBot,
goodie, crystal, marble, wall, peas, the exit, etc.) must be deleted and removed from the
StudentWorld’s container of active objects, resulting in an empty maze. If the user has
more lives left, our provided code will subsequently call your init() method to reload and
repopulate the maze and the level will then continue from scratch with a brand new set of
actors.
You must not call the cleanUp() method yourself when the player dies. Instead, this
method will be called by our code.
The Level Class and Level Data File
As mentioned, every level of Marble Madness has a different maze. The maze layout for
each level is stored in a data file, with the file level00.txt holding the details for the first
level’s maze, level01.txt holding the details for the second level’s maze, etc.
Here’s an example maze data file (you can modify our maze data files to create wacky
new levels, or add your own new maze data files to add new levels, if you like):
20
level00.txt:
###############
# @ v #
# b b #
# # ###
#o# b h#e#
#o#h b #*#
#a# b h#a#
#*#h b #o#
#a# b h#o#
###h b # #
# 2 #
# b #
#######o#######
#1 x r 2#
###############
As you can see, the data file contains a 15x15 grid of different characters that represent
the different actors in the level. Valid characters for your maze data file are:
The @ character specifies the location of the player’s avatar when starting a level. The
player’s avatar should also restart at this location if the player dies and must replay the
current level.
The # character represents a wall. The perimeter of each maze MUST be surrounded
completely by Walls.
The h character represents a Horizontal RageBot, specifiying that a RageBot that moves
only horizontally starts at this location in the maze when the player starts or replays the
current level.
The v character represents a Vertical RageBot, specifying that a RageBot that moves only
vertically starts at this location in the maze when the player starts or replays the current
level.
The 1 character represents a ThiefBot factory that manufactures ThiefBots.
The 2 character represents a Mean ThiefBot factory that manufactures Mean ThiefBots.
The * character represents a crystal that the player needs to pick up to complete the level.
The e character represents an extra life goodie that grants the player an extra life (but
does NOT restore the player’s current health!).
The r character represents a restore health goodie that restores the player’s hit points to
100%.
21
The a character represents an ammo goodie that gives the player 20 additional peas.
The x character represents the level’s exit. The level’s exit will be visible/active only
after the player has gathered all of the crystals on the level.
The b character represents a marble.
The o (lower-case o) character represents a pit in the floor.
All space characters represent locations where the player’s avatar and robots may walk
within the maze.
The Level Class
We have graciously ☺ decided to provide you with a class that can load level data files
for you. The class is called Level and may be found in our provided Level.h file. Here’s
how you might use this class:
#include "Level.h" // required to use our provided class
void StudentWorld::someFunc()
{
Level lev(assetPath());
Level::LoadResult result = lev.loadLevel("level00.txt");
if (result == Level::load_fail_file_not_found)
cerr << "Could not find level00.txt data file\n";
else if (result == Level::load_fail_bad_format)
cerr << "Your level was improperly formatted\n";
else if (result == Level::load_success)
{
cerr << "Successfully loaded level\n";
Level::MazeEntry ge = lev.getContentsOf(5,10); // x=5, y=10
switch (ge)
{
case Level::empty:
cout << "5,10 is empty\n";
break;
case Level::exit:
cout << "5,10 is where the exit is\n";
break;
case Level::player:
cout << "5,10 is where the player starts\n";
break;
case Level::horiz_ragebot:
cout << "5,10 starts with a horiz. RageBot\n";
break;
case Level::vert_ragebot:
cout << "5,10 starts with a vertical RageBot\n";
break;
case Level::thiefbot_factory:
cout << "5,10 holds a ThiefBot factory\n";
break;
case Level::enraged_thiefbot_factory:
cout << "5,10 holds an enraged ThiefBot factory\n";
break;
case Level::wall:
22
cout << "Location 5,10 holds a wall\n";
break;
}
}
}
Hint: You will presumably want to use our Level class when loading the current level
specification in your StudentWorld’s init() method.
You Have to Create the Classes for All Actors
The Marble Madness game has a number of different game objects, including:
• The player’s Avatar
• RageBots (Horizontal and Vertical varieties)
• ThiefBots
• Mean ThiefBots
• Factories (for Regular and Mean ThiefBots)
• Peas (that can be shot by both the player and robots)
• Exits
• Walls
• Marbles
• Pits
• Crystals
• Extra Life Goodies
• Restore Health Goodies
• Ammo Goodies
Each of these game objects can occupy the maze and interact with other game objects
within the maze.
Now of course, many of your game objects will share things in common – for instance,
every one of the objects in the game (RageBots, the player, walls, marbles, etc.) has x,y
coordinates. Many game objects have the ability to perform an action (e.g., move or
shoot) during each tick of the game. Many of them can potentially be attacked (e.g., the
player, robots and marbles can be attacked by peas, pits can be filled with and “attacked”
by marbles, etc.) and could “die” during a tick. All of them need some attribute that
indicates whether or not they are still alive or they died during the current tick, etc.
It is therefore your job to determine the commonalities between your different game
objects and make sure to factor out common behaviors and traits and move these into
appropriate base classes, rather than duplicate these items across your derived classes –
this is in fact one of the tenets of object-oriented programming.
Your grade on this project will depend upon your ability to intelligently create a set of
classes that follow good object-oriented design principles. Your classes must never
23
duplicate code or data member – if you find yourself writing the same (or largely similar)
code across multiple classes, then this is an indication that you should define a common
base class and migrate this common functionality/data to the base class. Duplication of
code is a so-called code smell, a weakness in a design that often leads to bugs,
inconsistencies, code bloat, etc.
Hint: When you notice this specification repeating the same text nearly identically in the
following sections (e.g., in the Extra Life Goodie section and the Restore Health Goodie
section, or in the RageBot and ThiefBots sections) you must make sure to identify
common behaviors and move these into proper base classes. NEVER duplicate behaviors
across classes that can be moved into a base class!
You MUST derive all of your game objects directly or indirectly from a base class that
we provide called GraphObject, e.g.:
class Actor: public GraphObject
{
public:
…
};
class ThiefBot: public Actor
{
public:
…
};
class MeanThiefBot: public ThiefBot
{
public:
…
};
GraphObject is a class that we have defined that helps hide the ugly logic required to
graphically display your actors on the screen. If you don’t derive your classes from our
GraphObject base class, then you won’t see anything displayed on the screen! ☺
The GraphObject class provides the following methods that you may use:
GraphObject(int imageID, int startX, int startY,
Direction startDirection = none);
void setVisible(bool shouldIDisplay);
double getX() const;
double getY() const;
void moveTo(double x, double y);
Direction getDirection() const; // Directions: none, up, down, left, right
void setDirection(Direction d); // Directions: none, up, down, left, right
You may use any of these member functions in your derived classes, but you must not
use any other member functions found inside of GraphObject in your other classes (even
24
if they are public in our class). You must not redefine any of these methods in your
derived classes since they are not defined as virtual in our base class.
GraphObject(int imageID, int startX, int startY, Direction startDirection) is the
constructor for a new GraphObject. When you construct a new GraphObject, you must
specify an image ID that indicates how the GraphObject should be displayed on screen
(e.g., as a RageBot, a Player, a wall, etc.). You must also specify the initial x,y location of
the object. The x value may range from 0 to VIEW_WIDTH-1 inclusive, and the y value
may range from 0 to VIEW_HEIGHT-1 inclusive. Notice that you pass the coordinates as
x,y (i.e., column, row starting from bottom left, and not row, column). For those objects
for which the concept of a direction doesn’t apply (e.g., walls or crystals), you may leave
off the final Direction argument or may specify none; For those objects for which a
direction applies (i.e., the player, robots, peas), you must specify the initial direction the
object is facing, (i.e., left, right, up, or down – these constants are defined in the
GraphObject.h file).
One of the following IDs, found in GameConstants.h, must be passed in for the imageID
value:
IID_PLAYER
IID_RAGEBOT
IID_THIEFBOT
IID_MEAN_THIEFBOT
IID_ROBOT_FACTORY
IID_PEA
IID_EXIT
IID_WALL
IID_MARBLE
IID_PIT
IID_CRYSTAL
IID_RESTORE_HEALTH
IID_EXTRA_LIFE
IID_AMMO
New GraphObjects start out invisible and are NOT displayed on the screen until the
programmer calls the setVisible() method with a value of true for the parameter.
setVisible(bool shouldIDisplay) is used to tell our graphical system whether or not to
display a particular GraphObject on the screen. If you call setVisible(true) on a
GraphObject, then your object will be displayed on screen automatically by our
framework (e.g., a RageBot image will be drawn to the screen at the GraphObject’s
specified x,y coordinates if the object’s Image ID is IID_RAGEBOT). If you call
setVisible(false) then your GraphObject will not be displayed on the screen. When you
create a new game object, always remember to call the setVisible() method with a value
of true or the actor won’t display on screen!
getX() and getY() are used to determine a GraphObject’s current location in the maze.
Since each GraphObject maintains its x,y location, this means that your derived classes
25
MUST NOT also have x,y member variables, but instead use these fucntions and
moveTo() from the GraphObject base class.
moveTo(int x, int y) is used to update the location of a GraphObject within the maze. For
example, if a RageBot’s movement logic dictates that it should move to the right, you
could do the following:
moveTo(getX()+1, y); // move one square to the right
You must use the moveTo() method to adjust the location of a game object if you want
that object to be properly animated. As with the GraphObject constructor, note that the
order of the parameters to moveTo is x,y (col,row) and NOT y,x (row,col).
getDirection() is used to determine the direction a GraphObject is facing. For example, a
pea fired by a robot must travel in the direction the robot is facing, so you can use this
method to learn that direction.
setDirection(Direction d) is used to change the direction a GraphObject is facing. For
example, when the user presses the up arrow, you can use this method to cause the
player’s avatar to be displayed facing up, as well as causing any peas the player fires to
travel upward.
The player
Here are the requirements you must meet when implementing the player’s Avatar class.
What the Avatar Must Do When It Is Created
When it is first created:
1. The Avatar object must have an image ID of IID_PLAYER.
2. The player must always start at the proper location as specified by the current
level’s data file. Hint: Since your StudentWorld's init() function loads the level, it
knows this x,y location to pass when constructing the Avatar object.
3. The player, in its initial state:
a. Has 20 hit points.
b. Has 20 peas.
c. Faces right.
In addition to any other initialization that you decide to do in your Player class, a Player
object must make itself visible using the GraphObject class’s setVisible() method,
perhaps by calling setVisible(true).
26
What the Avatar Must Do During a Tick
The Avatar must be given an opportunity to do something during every tick (in its
doSomething() method). When given an opportunity to do something, the player must do
the following:
1. The Avatar must check to see if it is currently alive. If not, then the Avatar’s
doSomething() method must return immediately – none of the following steps
should be performed.
2. Otherwise, the doSomething() method must check to see if the user pressed a key
(the section below shows how to check this). If the user pressed a key:
a. If the user pressed the Escape key, the user is asking to abort the current
level. In this case, the Avatar object should set itself to dead. The code in
the StudentWorld class should detect that the player has died and address
this appropriately (e.g., replay the level from scratch, or end the game).
b. If the user pressed the space bar, then if the Avatar has any peas, the it will
fire a pea, which reduces their pea count by 1. To fire a pea, a new pea
object must be added at the square immediately in front of the player’s
avatar, facing the same direction as the avatar. For example, if the Avatar
is at x=10,y=7 facing upward, then the pea would be created at location
x=10, y=8 facing upward. When the Avatar fires a pea, it must play the
SOUND_PLAYER_FIRE sound effect (see the StudentWorld section of
this document for details on how to play a sound).
Hint: When you create a new pea object in the proper location and facing
the proper direction, give it to the StudentWorld to manage (e.g., animate)
along with the other game objects.
c. If the user asks to move up, down, left or right by pressing a directional
key, then the Avatar’s direction should be adjusted to the indicated
direction (e.g., if the Avatar were facing upward, and the user hit the right
arrow key, the player’s direction should be adjusted to right). Then, the
Avatar will try to move to the adjacent square in the direction it is facing
if that square does not contain an obstruction: a marble that cannot be
pushed, a wall, a pit, a robot, or a robot factory. If the player can move, it
must update its location with the GraphObject class’s moveTo() method.
For information on how and when marbles can be pushed, see the Marble
section of this document. A marble can be pushed only in the direction the
player is facing, and only if the square adjacent to the marble in that
direction is empty or has a pit. If the player tries to move in the direction
of an adjacent marble that can be pushed, it calls a push method defined in
the marble class to cause the marble’s position to be adjusted, and the
player updates its location to the square formerly occupied by the marble.
27
What the player Must Do When It Is Attacked
When the Avatar is attacked (i.e., a pea collides with him/her), the Avatar’s hit points
must be decremented by 2 points. If the Avatar still has hit points after this, then the
game must play an impact sound effect: SOUND_PLAYER_IMPACT; otherwise, the
game must set the Avatar’s state to dead and play a death sound effect:
SOUND_PLAYER_DIE.
Getting Input From the User
Since Marble Madness is a real-time game, you can’t use the typical getline or cin
approach to get a user's key press within the player’s doSomething() method— that would
stop your program and wait for the user to type something and then hit the Enter key.
This would make the game awkward to play, requiring the user to hit a directional key
then hit Enter, then hit a directional key, then hit Enter, etc. Instead of this approach, you
will use a function called getKey() that we provide in our GameWorld class (from which
your StudentWorld class is derived) to get input from the user1
. This function rapidly
checks to see if the user has hit a key. If so, the function returns true and the int variable
passed to it is set to the code for the key. Otherwise, the function immediately returns
false, meaning that no key was hit. This function could be used as follows:
void Player::doSomething()
{
...
int ch;
if (getWorld()->getKey(ch))
{
// user hit a key this tick!
switch (ch)
{
case KEY_PRESS_LEFT:
... move avatar to the left ...;
break;
case KEY_PRESS_RIGHT:
... move avatar to the right ...;
break;
case KEY_PRESS_SPACE:
... add a pea in the square in front of the avatar...;
break;
// etc…
}
}
...
}
Wall
1 Hint: Since your Avatar class will need to access the getKey() method in the GameWorld class (which is
the base class for your StudentWorld class), your Avatar class (or more likely, one of its base classes) will
need a way to obtain a pointer to the StudentWorld object it's playing in. If you look at our code example,
you’ll see how the player’s doSomething() method first gets a pointer to its world via a call to getWorld() (a
method in one of its base classes that returns a pointer to a StudentWorld), and then uses this pointer to call
the getKey() method.
28
Walls don’t really do much. They just sit still in place. Here are the requirements you
must meet when implementing the Wall class.
What a Wall Must Do When It Is Created
When it is first created:
1. A wall object must have an image ID of IID_WALL.
2. A wall must always start at the proper location as specified by the current level’s
data file.
3. A wall has no direction (Hint: Its ancestor GraphObject base object always has
the direction none).
In addition to any other initialization that you decide to do in your Wall class, a wall
object must make itself visible using the GraphObject class’s setVisible() method,
perhaps by calling setVisible(true).
What a Wall Must Do During a Tick
A wall must be given an opportunity to do something during every tick (in its
doSomething() method). When given an opportunity to do something, the wall must do
nothing. After all, it’s just a wall! (What would you think it would do?)
What a Wall Must Do When It Is Attacked
When a wall is attacked (i.e., a pea collides with it), nothing happens to the wall.
Marble
You must create a class to represent a giant marble. Marbles may be pushed around by
the player so long as there’s nothing in the way. Here are the requirements you must meet
when implementing the Marble class.
What a Marble Must Do When It Is Created
When it is first created:
1. A marble object must have an image ID of IID_MARBLE.
2. A marble must always start at the proper location as specified by the current
level’s data file.
3. A marble starts out with 10 hit points.
4. A marble has no direction.
In addition to any other initialization that you decide to do in your Marble class, a marble
object must make itself visible using the GraphObject class’s setVisible() method,
perhaps by calling setVisible(true).
29
What a Marble Must Do During a Tick
Each time a marble is asked to do something (during a tick), it must do nothing. After all,
it’s a marble.
What a Marble Must Do When It Is Attacked
When a marble is attacked (i.e., a pea collides with it), it loses 2 hit points. When its hit
points reach zero, it must “die” and be removed from the game. In its place will be an
empty spot.
What a Marble Must Do When It Is Pushed
When a marble is pushed by the player in a particular direction:
1. It must check to see if it can be moved in the specified direction (see the rules
about where marbles can be pushed below)
2. If the marble’s destination is an empty spot or a pit, then the marble should move
itself to that spot when pushed (the pit will take care of swallowing the marble –
see the pit section for more details).
3. Otherwise, the marble can’t be moved.
Rules about How/When Marbles Can Be Pushed
For purposes of explanation, assume @ represents the player, who is trying to move
rightward. Further let’s assume M represents a marble, # represents a wall, O represents
a pit, and _ represents an empty square, and ? represents all other objects (e.g., Goodies,
robots, Crystals, the exit (regardless of whether it is hidden or revealed), etc.).
Here are several scenarios and their results:
Initial State Final State Notes
@M_ _@M The player moved right, pushing the
marble right into the far open slot.
@MO _@_ The player moved right, pushing the
marble into the pit at the far-right,
which was then filled and replaced
by an empty square
@MM @MM The player can’t move right since
there’s no open spot to push the
middle marble (the far-right marble
is in the way!).
@M# @M# The player can’t move right since
there’s no open spot to push the
marble (there’s a wall in the way).
30
@M? @M? The player can’t move right since
there’s no open spot to push the
marble (there’s a robot, factory,
goodie, exit, etc. in the way).
Pea
You must create a class to represent a pea. Here are the requirements you must meet
when implementing the Pea class.
What a Pea Must Do When It Is Created
When it is first created:
1. A pea object must have an image ID of IID_PEA.
2. A pea must have its x,y location specified for it – the player or robot that fires the
pea must pass in this x,y location when constructing a pea object.
3. A pea must have its direction specified for it – the player or robot that fires the
pea must pass in this direction when constructing the pea object.
In addition to any other initialization that you decide to do in your Pea class, a pea object
must make itself visible using the GraphObject class’s setVisible() method, perhaps by
calling setVisible(true).
What a Pea Must Do During a Tick
Each time a pea object is asked to do something (during a tick):
1. The pea must check to see if it is currently alive. If not, then its doSomething()
method must return immediately – none of the following steps should be
performed.
2. Otherwise, the pea must check to see if any other objects are on its current square:
a. If the pea is on the same square as a marble, a robot, or the player, then the
pea must:
i. Damage the object appropriately (by causing the object to lose 2
hit points) – each damaged object can then deal with this damage
in its own unique way (this may result in the damaged object
dying/disappearing, a sound effect being played, the user possibly
getting points, etc.) Hint: The pea can tell the object that it has
been damaged by calling a method that object has (presumably
named damage or something similar) and the target object can then
determine what impact, if any, this will have.
ii. Set its own state to dead (so that it will be removed from the game
by the StudentWorld object at the end of the current tick).
31
iii. Do nothing else during the current tick.
b. If the pea is on the same square as a wall or a robot factory, it will simply
hit the wall or factory and do no damage. In this case, the pea must set its
own state to dead (so that it will be removed from the game by the
StudentWorld object at the end of the current tick). The pea must then do
nothing more during the current tick. Note: If a pea finds itself on a square
with both a robot and a factory, then the pea must damage the robot.
c. If the pea is on the same square as any other game object (e.g., another
Pea, the exit, a pit, a goodie, etc.) or on an empty square by itself, it does
not interact with the other object in any way. Continue with step #3.
3. The pea must move itself one square in its initially-specified direction.
4. Finally, after the pea has moved itself, the pea must check to see if there are
objects are on its new square where it just moved, using the same algorithm
described in step #2 above. If so, it must perform the same behavior as described
in step #2 (e.g., damage the object, etc.), but does not move any further during
this tick.
What a Pea Must Do When It Is Attacked
Peas can’t be attacked, silly. Peas will pass right through other peas.
Pit
You must create a class to represent a pit in the ground. When a marble is moved onto the
same square as a pit, the pit will be filled in by swallowing up the marble. Here are the
requirements you must meet when implementing the Pit class.
What a Pit Must Do When It Is Created
When it is first created:
1. A pit object must have an image ID of IID_PIT.
2. A pit must always start at the proper location as specified by the current level’s
data file.
3. A pit has no direction.
In addition to any other initialization that you decide to do in your Pit class, a pit object
must make itself visible using the GraphObject class’s setVisible() method, perhaps by
calling setVisible(true).
What a Pit Must Do During a Tick
Each time a pit object is asked to do something (during a tick):
32
1. The pit must check to see if it is alive. If not, then its doSomething() method must
return immediately – none of the following steps should be performed.
2. Otherwise, if a marble is on the same square as the pit, then the pit must:
a. Set its state to dead (so that it will be removed from the game by the
StudentWorld object at the end of the current tick).
b. Set the marble’s state to dead (so that it will be removed from the game by
the StudentWorld object at the end of the current tick).
This results in the pit swallowing up the marble, and both disappearing.
What a Pit Must Do When It Is Attacked
Pits can’t be attacked. Peas will pass right over them.
Crystal
You must create a class to represent a crystal. When the player picks up a Crystal (by
moving onto the same square as it), they get 50 points. Here are the requirements you
must meet when implementing the crystal class.
What a Crystal Must Do When It Is Created
When it is first created:
1. A crystal object must have an image ID of IID_CRYSTAL.
2. A crystal must always start at the proper location as specified by the current
level’s data file.
3. A crystal has no direction.
In addition to any other initialization that you decide to do in your Crystal class, a crystal
object must make itself visible using the GraphObject class’s setVisible() method,
perhaps by calling setVisible(true).
What a Crystal Must Do During a Tick
Each time a crystal is asked to do something (during a tick):
1. The crystal must check to see if it is currently alive. If not, then its doSomething()
method must return immediately – none of the following steps should be
performed.
2. Otherwise, if the player is on the same square as the crystal, then the crystal must:
a. Inform the StudentWorld object that the user is to receive 50 more points.
It can do this by using StudentWorld’s increaseScore() method (inherited
from GameWorld).
b. Set its state to dead (so that it will be removed from the game by the
StudentWorld object at the end of the current tick).
33
c. Play a sound effect to indicate that the player picked up the crystal:
SOUND_GOT_GOODIE.
What a Crystal Must Do When It Is Attacked
Crystals can’t be attacked. Peas will pass right over them.
The Exit
You must create a class to represent the exit, the doorway the avatar must step on to
complete the current level, but only after collecting all the crystals on the level.
Here are the requirements you must meet when implementing the exit class.
What the Exit Must Do When It Is Created
When it is first created:
1. The exit object must have an image ID of IID_EXIT.
2. The exit must always start at the proper location as specified by the current level’s
data file.
3. The exit has no direction.
4. The exit must start out invisible when it is created, since it is revealed to the
player only after the player has collected all of the crystals on the level. At that
point, the exit object must be made visible, and then the player can complete the
level by stepping on it.
What the Exit Must Do During a Tick
Each time the exit object is asked to do something (during a tick):
1. If the player is currently on the same square as the exit AND the exit is visible
(because the player has collected all of the crystals on the level), then the exit
must:
a. Play a sound indicating that the player finished the level:
SOUND_FINISHED_LEVEL.
b. Inform the StudentWorld object that the user is to receive 2000 more
points for using the exit.
c. Inform the StudentWorld object somehow that the player has
completed the current level.
d. If there are any bonus points left, the player must receive the bonus
points for completing the level quickly. It’s your choice whether the
StudentWorld object or the exit object grants the points.
What the Exit Must Do When It Is Attacked
34
The Exit can’t be attacked. Peas will pass right over it.
What the Exit Must Do When It Is Revealed
If the player collects all of the crystals on the current level of the game, then the game
must make the exit on that level visible, allowing it to be used by the player to exit the
current level and advance to the next level.
You could either have your StudentWorld’s move() method detect whether all the crystals
have been collected, and if so, make the exit object visible, or you could have the exit
object’s doSomething() method determine whether all the crystals have been collected,
and if so, reveal itself. This design decision is up to you.
When the exit is revealed, it must transition from being invisible (it starts in this state) to
visible, and must play a reveal sound effect: SOUND_REVEAL_EXIT.
Note: The transition of the exit on a level from invisible to visible must happen only
once, when the last remaining crystal on the current level has been picked up by the
player. On the subsequent ticks until the player uses the exit, your program must not
play the reveal sound effect again.
Extra Life Goodie
You must create a class to represent an extra life goodie. When the player picks up this
goodie (by moving onto the same square as it), it gives the player an extra life! Here are
the requirements you must meet when implementing the Extra Life Goodie class.
What an Extra Life Goodie Must Do When It Is Created
When it is first created:
1. The extra life goodie object must have an image ID of IID_EXTRA_LIFE.
2. An extra life goodie must always start at the proper location as specified by the
current level’s data file.
3. Extra life goodies have no direction.
In addition to any other initialization that you decide to do in your Extra Life Goodie
class, an extra life goodie object must make itself visible using the GraphObject class’s
setVisible() method, perhaps by calling setVisible(true).
What an Extra Life Goodie Must Do During a Tick
Each time an extra life goodie is asked to do something (during a tick):
35
1. The extra life goodie must check to see if it is currently alive. If not, then its
doSomething() method must return immediately – none of the following steps
should be performed.
2. Otherwise, if the player is on the same square as the extra life goodie, then the
extra life goodie must:
a. Inform the StudentWorld object that the user is to receive 1000 more
points.
b. Set its state to dead (so that it will be removed from the game by the
StudentWorld object at the end of the current tick).
c. Play a sound effect to indicate that the player picked up the goodie:
SOUND_GOT_GOODIE.
d. Inform the StudentWorld object that the player is to gain one extra life.
What an Extra Life Goodie Must Do When It Is Attacked
Extra life goodies can’t be attacked. Peas will pass right over them.
Restore Health Goodie
You must create a class to represent a restore health goodie. When the player picks up
this goodie (by moving onto the same square as it), it restores the player’s health. Here
are the requirements you must meet when implementing the Restore Health Goodie class.
What a Restore Health Goodie Must Do When It Is Created
When it is first created:
1. The restore health goodie object must have an image ID of
IID_RESTORE_HEALTH.
2. A restore health goodie must always start at the proper location as specified by the
current level’s data file.
3. Restore health goodies have no direction.
In addition to any other initialization that you decide to do in your Restore Health Goodie
class, a restore health goodie object must make itself visible using the GraphObject
class’s setVisible() method, perhaps by calling setVisible(true).
What a Restore Health Goodie Must Do During a Tick
Each time a restore health goodie is asked to do something (during a tick):
1. The restore health goodie must check to see if it is currently alive. If not, then its
doSomething() method must return immediately – none of the following steps
should be performed.
36
2. Otherwise, if the Avatar is on the same square as the restore health goodie, then
the restore health goodie must:
a. Inform the StudentWorld object that the user is to receive 500 more points.
b. Set its state to dead (so that it will be removed from the game by the
StudentWorld object at the end of the current tick).
c. Play a sound effect to indicate that the player picked up the Goodie:
SOUND_GOT_GOODIE.
d. Inform the Avatar object that the player is to restore itself to full health
(i.e., 20 hit points, the initial value).
What a Restore Health Goodie Must Do When It Is Attacked
Restore health goodies can’t be attacked. Peas will pass right over them.
Ammo Goodie
You must create a class to represent an ammo goodie. When the player picks up this
goodie (by moving onto the same square as it), it adds 20 peas to the player pea supply.
Here are the requirements you must meet when implementing the Ammo Goodie class.
What an Ammo Goodie Must Do When It Is Created
When it is first created:
1. The ammo goodie object must have an image ID of IID_AMMO.
2. An ammo goodie must always start at the proper location as specified by the
current level’s data file.
3. Ammo goodies have no direction.
In addition to any other initialization that you decide to do in your Ammo Goodie class,
an ammo goodie object must make itself visible using the GraphObject class’s
setVisible() method, perhaps by calling setVisible(true).
What an Ammo Goodie Must Do During a Tick
Each time an ammo goodie is asked to do something (during a tick):
1. The ammo goodie must check to see if it is currently alive. If not, then its
doSomething() method must return immediately – none of the following steps
should be performed.
2. Otherwise, if the player is on the same square as the ammo goodie, then the ammo
goodie must:
a. Inform the StudentWorld object that the user is to receive 100 more points.
b. Set its state to dead (so that it will be removed from the game by the
StudentWorld object at the end of the current tick).
37
c. Play a sound effect to indicate that the player picked up the goodie:
SOUND_GOT_GOODIE.
d. Inform the player that the player is to add 20 peas.
What an Ammo Goodie Must Do When It Is Attacked
Ammo goodies can’t be attacked. Peas will pass right over them.
RageBot
You must create a class to represent a RageBot. Here are the requirements you must meet
when implementing the RageBot class.
What a RageBot Must Do When It Is Created
When it is first created:
1. The RageBot object must have an image ID of IID_RAGEBOT.
2. A RageBot must always start at the proper location as specified by the current
level’s data file.
3. A RageBot must start out facing either right or down, depending on whether it’s a
horizontal or vertical RageBot.
4. A RageBot must start out with 10 hit points.
In addition to any other initialization that you decide to do in your RageBot class, a
RageBot object must make itself visible using the GraphObject class’s setVisible()
method, perhaps by calling setVisible(true).
A RageBot, unlike the player, doesn’t necessarily get to take an action during every tick
of the game. (This is to make the game easier to play, since if a RageBot moved once
every tick, it would move much faster than the typical user can think and hit the keys on
the keyboard.) A RageBot must therefore compute a value indicating how frequently it’s
allowed to take an action. This value is to be computed as follows:
int ticks = (28 – levelNumber) / 4; // levelNumber is the current
// level number (0, 1, 2, etc.)
if (ticks < 3)
ticks = 3; // no RageBot moves more frequently than this
If the value of ticks is 5, for example, then the RageBot must “rest” for 4 ticks, perform
its normal behavior on the next tick, rest for 4 ticks, perform its behavior on the next tick,
etc., performing its behavior every 5th tick.
What a RageBot Must Do During a Tick
Each time a RageBot is asked to do something (during a tick):
38
1. The RageBot must check to see if it is currently alive. If not, then its
doSomething() method must return immediately – none of the following steps
should be performed.
2. If the RageBot is supposed to “rest” during the current tick, it must during the
current tick other than to update its tick count.
3. Otherwise, the RageBot must determine whether it should fire its pea cannon: If
the player is in the same row or column as the RageBot AND the RageBot is
currently facing the player AND there are no obstacles (specifically, walls,
marbles, robots, or robot factories) in the way, then the RageBot will fire a pea
toward the player and then do nothing more during the current tick.
To fire a pea, a new pea object must be added at the square immediately in front
of the RageBot, facing the same direction as the RageBot. For example, if the
RageBot is at x=10,y=7 facing upward, then the pea would be created at location
x=10, y=8 facing upward. Every time a RageBot fires a pea, it must play the
SOUND_ENEMY_FIRE sound effect.
Hint: When you create a new pea object in the proper location and facing the
proper direction, give it to your StudentWorld to manage (e.g., animate) along
with the other game objects.
4. Otherwise, the RageBot will try to move to the adjacent square in the direction it
is facing if that square does not contain an obstruction: the player, a marble, a
wall, a pit, a robot, or a robot factory.
a. If the square does not contain an obstruction, the RageBot will move to it.
b. Otherwise, the RageBot will not move, but instead reverse the direction
it’s facing (left → right or up→down).
What a RageBot Must Do When It Is Attacked
A RageBot should have a method named damage or something similar that a pea object
can call to inform the RageBot that it has been damaged. When a RageBot is damaged:
1. If its hit points have not reached zero, the game must play the
SOUND_ROBOT_IMPACT sound effect.
2. Otherwise, the RageBot has been killed, so it must:
a. Set its own state to dead (so that it will be removed from the game by the
StudentWorld object at the end of the current tick).
b. Play a sound indicating that it has died: SOUND_ROBOT_DIE.
c. Inform the StudentWorld object that the user is to receive 100 more points.
ThiefBot
You must create a class to represent a ThiefBot. Here are the requirements you must meet
when implementing the ThiefBot class.
39
What a ThiefBot Must Do When It Is Created
When it is first created:
1. The ThiefBot object must have an image ID of IID_THIEFBOT.
2. A ThiefBot must always start at the proper location as specified by the current
level’s data file.
3. A ThiefBot must start out facing right.
4. A ThiefBot must start out with 5 hit points.
5. A ThiefBot selects a random integer from 1 to 6 inclusive, which we’ll call
distanceBeforeTurning. This is how far the ThiefBot will move in a straight line
(if it can) before turning.
In addition to any other initialization that you decide to do in your ThiefBot class, a
ThiefBot object must make itself visible using the GraphObject class’s setVisible()
method, perhaps by calling setVisible(true).
A ThiefBot, unlike the player, doesn’t necessarily get to take an action during every tick
of the game. (This is to make the game easier to play, since if a ThiefBot moved once
every tick, it would move much faster than the typical user can think and hit the keys on
the keyboard.) A ThiefBot must therefore compute a value indicating how frequently it’s
allowed to take an action. This value is to be computed as follows:
int ticks = (28 – levelNumber) / 4; // levelNumber is the current
// level number (0, 1, 2, etc.)
if (ticks < 3)
ticks = 3; // no ThiefBot moves more frequently than this
If the value of ticks is 5, for example, then the ThiefBot must “rest” for 4 ticks, perform
its normal behavior on the next tick, rest for 4 ticks, perform its behavior on the next tick,
etc., performing its behavior every 5th tick.
What a ThiefBot Must Do During a Tick
Each time a ThiefBot is asked to do something (during a tick):
1. The ThiefBot must check to see if it is currently alive. If not, then its
doSomething() method must return immediately – none of the following steps
should be performed.
2. If the ThiefBot is supposed to “rest” during the current tick, it must do nothing
during the current tick other than to update its tick count.
3. Otherwise, if the ThiefBot is on the same square as a goodie, and has not yet ever
picked up a goodie, then there is a 1 in 10 chance that it will pick up that goodie.
If it does:
a. That Goodie must no longer appear on the screen and must not be able to
be picked up by the player until the ThiefBot is killed.
40
b. The ThiefBot must play a “munching” sound:
SOUND_ROBOT_MUNCH.
c. The ThiefBot must do nothing more during the current tick.
Hint: You can either (1) make the goodie invisible and unable to be picked up
while the ThiefBot is carrying it, undoing that when the ThiefBot is killed, or (2)
have the ThiefBot remember the kind of goodie it picked up, kill the goodie
object, and when the ThiefBot is killed, create a new goodie object of that type.
When the ThiefBot is killed, the goodie must appear at the ThiefBot’s location.
4. Otherwise, if the ThiefBot has not yet moved distanceBeforeTurning squares in
its current direction, then it will try to move to the adjacent square in the direction
it is facing if that square does not contain an obstruction: the player, a marble, a
wall, a pit, a robot, a robot Factory, or the player. If the ThiefBot can move, it
must update its location to that square and do nothing more during the curent tick.
5. Otherwise, the ThiefBot has either moved distanceBeforeTurning squares in a
straight line or encountered an obstruction. In this case, the ThiefBot must:
a. Select a random integer from 1 to 6 inclusive to be the new value of
distanceBeforeTurning.
b. Select a random direction, which we’ll call d. It’s OK if this direction
happens to be the same as the ThiefBot’s current direction.
c. Starting by considering the direction d, repeat the following until it has
either successfully moved or cannot move because of obstructions in all
four directions:
i. Determine whether there is an obstruction in the adjacent square in
the direction under consideration. If there is none, the ThiefBot
must set its direction to the direction under consideration, move to
that square, and then do nothing more during the current tick.
ii. If there is an obstruction, the ThiefBot must consider a direction it
has not already considered during this loop, and repeat step i.
If there is an obstruction in all four directions, then the ThiefBot must set
its current direction to d and do nothing more during the current tick.
What a ThiefBot Must Do When It Is Attacked
A ThiefBot should have a method named damage or something similar that a pea object
can call to inform the ThiefBot that it has been damaged. When a ThiefBot is damaged:
1. If its hit points have not reached zero, the game must play the
SOUND_ROBOT_IMPACT sound effect.
2. Otherwise, the ThiefBot has been killed, so it must:
a. If it had picked up a goodie, make that goodie appear on the screen and be
able to be picked up by the player.
b. Set its own state to dead (so that it will be removed from the game by the
StudentWorld object at the end of the current tick).
c. Play a sound indicating that it has died: SOUND_ROBOT_DIE.
d. Inform the StudentWorld object that the user is to receive 10 more points.
41
Mean ThiefBot
You must create a class to represent a Mean ThiefBot. Here are the requirements you
must meet when implementing the Mean ThiefBot class.
What a Mean ThiefBot Must Do When It Is Created
When it is first created:
1. The Mean ThiefBot object must have an image ID of IID_MEAN_THIEFBOT.
2. A Mean ThiefBot must always start at the proper location as specified by the
current level’s data file.
3. A Mean ThiefBot must start out facing right.
4. A Mean ThiefBot must start out with 8 hit points.
5. A Mean ThiefBot selects a random integer from 1 to 6 inclusive, which we’ll call
distanceBeforeTurning. This is how far the Mean ThiefBot will move in a
straight line (if it can) before turning.
In addition to any other initialization that you decide to do in your Mean ThiefBot class, a
Mean ThiefBot object must make itself visible using the GraphObject class’s setVisible()
method, perhaps by calling setVisible(true).
A Mean ThiefBot, unlike the player, doesn’t necessarily get to take an action during
every tick of the game. (This is to make the game easier to play, since if a Mean
ThiefBot moved once every tick, it would move much faster than the typical user can
think and hit the keys on the keyboard.) A Mean ThiefBot must therefore compute a
value indicating how frequently it’s allowed to take an action. This value is to be
computed as follows:
int ticks = (28 – levelNumber) / 4; // levelNumber is the current
// level number (0, 1, 2, etc.)
if (ticks < 3)
ticks = 3; // no Mean ThiefBot moves more frequently than this
If the value of ticks is 5, for example, then the Mean ThiefBot must “rest” for 4 ticks,
perform its normal behavior on the next tick, rest for 4 ticks, perform its behavior on the
next tick, etc., performing its behavior every 5th tick.
What a Mean ThiefBot Must Do During a Tick
Each time a Mean ThiefBot is asked to do something (during a tick):
1. The Mean ThiefBot must check to see if it is currently alive. If not, then its
doSomething() method must return immediately – none of the following steps
should be performed.
42
2. If the Mean ThiefBot is supposed to “rest” during the current tick, it must do
nothing during the current tick other than to update its tick count.
3. Otherwise, the Mean ThiefBot must determine whether it should fire its pea
cannon: If the player is in the same row or column as the Mean ThiefBot AND
the Mean ThiefBot is currently facing the player AND there are no obstacles
(specifically, Walls, Marbles, robots, or robot factories) in the way, then the Mean
ThiefBot will fire a pea toward the player and then do nothing more during the
current tick.
To fire a pea, a new pea object must be added at the square immediately in front
of the Mean ThiefBot, facing the same direction as the Mean ThiefBot. For
example, if the Mean ThiefBot is at x=10,y=7 facing upward, then the Pea would
be created at location x=10, y=8 facing upward. Every time a Mean ThiefBot fires
a pea, it must play the SOUND_ENEMY_FIRE sound effect.
Hint: When you create a new pea object in the proper location and facing the
proper direction, give it to your StudentWorld to manage (e.g., animate) along
with the other game objects.
4. Otherwise, if the Mean ThiefBot is on the same square as a goodie, and has not
yet ever picked up a goodie, then there is a 1 in 10 chance that it will pick up that
Goodie. If it does:
a. That Goodie must no longer appear on the screen and must not be able to
be picked up by the player until the Mean ThiefBot is killed.
b. The Mean ThiefBot must play a “munching” sound:
SOUND_ROBOT_MUNCH.
c. The Mean ThiefBot must do nothing more during the current tick.
Hint: You can either (1) make the Goodie invisible and unable to be picked up
while the Mean ThiefBot is carrying it, undoing that when the Mean ThiefBot is
killed, or (2) have the Mean ThiefBot remember the kind of goodie it picked up,
kill the goodie object, and when the Mean ThiefBot is killed, create a new Goodie
object of that type. When the Mean ThiefBot is killed, the goodie must appear at
the Mean ThiefBot’s location.
5. Otherwise, if the Mean ThiefBot has not yet moved distanceBeforeTurning
squares in its current direction, then it will try to move to the adjacent square in
the direction it is facing if that square does not contain an obstruction: the player,
a marble, a wall, a pit, a robot, a robot factory, or the player. If the Mean
ThiefBot can move, it must update its location to that square and do nothing more
during the current tick.
6. Otherwise, the Mean ThiefBot has either moved distanceBeforeTurning squares
in a straight line or encountered an obstruction. In this case, the Mean ThiefBot
must:
a. Select a random integer from 1 to 6 inclusive to be the new value of
distanceBeforeTurning.
43
b. Select a random direction, which we’ll call d. It’s OK if this direction
happens to be the same as the Mean ThiefBot’s current direction.
c. Starting by considering the direction d, repeat the following until it has
either successfully moved or cannot move because of obstructions in all
four directions:
i. Determine whether there is an obstruction in the adjacent square in
the direction under consideration. If there is none, the Mean
ThiefBot must set its direction to the direction under consideration,
move to that square, and then do nothing more during the current
tick.
ii. If there is an obstruction, the Mean ThiefBot must consider a
direction it has not already considered during this loop, and repeat
step i.
If there is an obstruction in all four directions, then the Mean ThiefBot
must set its current direction to d and do nothing more during the current
tick.
What a Mean ThiefBot Must Do When It Is Attacked
A Mean ThiefBot should have a method named damage or something similar that a pea
object can call to inform the Mean ThiefBot that it has been damaged. When a Mean
ThiefBot is damaged:
1. If its hit points have not reached zero, the game must play the
SOUND_ROBOT_IMPACT sound effect.
2. Otherwise, the Mean ThiefBot has been killed, so it must:
a. If it had picked up a goodie, make that goodie appear on the screen and be
able to be picked up by the player.
b. Set its own state to dead (so that it will be removed from the game by the
StudentWorld object at the end of the current tick).
c. Play a sound indicating that it has died: SOUND_ROBOT_DIE.
d. Inform the StudentWorld object that the user is to receive 20 more points.
ThiefBot Factory
You must create a class to represent a ThiefBot Factory. ThiefBot Factories come in two
forms: one that manufactures regular ThiefBots and one that manufactures Mean
ThiefBots. A ThiefBot Factory churns out new ThiefBots and adds them to the current
level according to rules described below. Here are the requirements you must meet when
implementing the ThiefBot class.
What a ThiefBot Factory Must Do When It Is Created
When it is first created:
1. The ThiefBot factory object must have an image ID of IID_ROBOT_FACTORY.
44
2. A ThiefBot factory must always start at the proper location as specified by the
current level’s data file.
3. A ThiefBot factory has no direction.
4. A ThiefBot factory is told whether it is to produce regular ThiefBots or Mean
ThiefBots.
In addition to any other initialization that you decide to do in your ThiefBot factory class,
a ThiefBot factory object must make itself visible using the GraphObject class’s
setVisible() method, perhaps by calling setVisible(true).
What a ThiefBot Factory Must Do During a Tick
Each time a ThiefBot factory is asked to do something (during a tick):
1. The ThiefBot factory must count how many ThiefBots of either type are present
in the square region that extends from itself 3 squares up, 3 squares left through 3
squares down, 3 squares right. For example, a factory at x=10,y=8 would count
all the ThiefBots and Mean ThiefBots in the region bounded by x=7,y=11;
x=13,y=11; x=13,y=5; and x=7,y=5, inclusive. A factory at x=2,y=13 would
count the ThiefBots and Mean ThiefBots in the region bounded by x=0,y=14;
x=5,y=14; x=5,y=10; and x=0,y=10, inclusive, properly accounting for the
boundaries of the maze.
2. If the count is less than 3 AND there is no ThiefBot of any type on the same
square as the factory, then there is a 1 in 50 chance that during the current tick,
the Factory will create a new ThiefBot of the type that factory manufactures and
add it to the maze on the same square as the factory. If it creates a new ThiefBot,
it must play the SOUND_ROBOT_BORN sound effect.
What a ThiefBot Factory Must Do When It Is Attacked
ThiefBot factory can’t be attacked. Peas that smack into them do no damage to them.
However, if a ThiefBot of any type is on the same square as a factory (because it was just
created by the Factory), then a pea that moves onto that square damages the ThiefBot as
usual.
Object Oriented Programming Best Practices
Before designing your base and derived classes for Project 3 (or for that matter, any other
school or work project), make sure to consider the following best practices. These tips
will help you not only write a better object-oriented program, but also help you get a
better grade on P3!
45
Try your best to leverage the following best practices in your program, but don’t be
overly obsessive – it’s rarely possible to make a set of perfect classes. That’s often a
waste of time. Remember, the best is the enemy of the good (enough).
Here we go!
1. You MUST NOT use the imageID (e.g., IID_RAGEBOT, IID_PLAYER,
IID_WALL, etc.) to determine the type of an object or store the imageID inside
any of your objects as a member variable. You may also not use any other
similar approach (e.g., a member string with the object’s name, an enumerated
type, etc.). Doing so will result in a score of ZERO for this project.
2. Avoid using dynamic cast to identify common types of objects. Instead add
methods to check for various classes of behaviors:
Don’t do this:
void decideWhetherToAddOil(Actor *p)
{
if (dynamic_cast<BadRobot *>(p) != nullptr ||
dynamic_cast<GoodRobot *>(p) != nullptr ||
dynamic_cast<ReallyBadRobot *>(p) != nullptr ||
dynamic_cast<StinkyRobot *>(p) != nullptr)
p->addOil();
}
Do this instead:
void decideWhetherToAddOil (Actor *p)
{
// define a common method, have all Robots return true, all
// biological organisms return false
if (p->requiresOilToOperate())
p->addOil();
}
3. Always avoid defining specific isParticularClass() methods for each type of
object. Instead add methods to check for various common behaviors that span
multiple classes:
Don’t do this:
void decideWhetherToAddOil (Actor *p)
{
if (p->isGoodRobot() || p->isBadRobot() || p->isStinkyRobot())
p->addOil();
}
Do this instead:
46
void decideWhetherToAddOil (Actor *p)
{
// define a common method, have all Robots return true, all
// biological organisms return false
if (p->requiresOilToOperate())
p->addOil();
}
4. If two related subclasses (e.g., SmellyRobot and GoofyRobot) each directly
define a member variable that serves the same purpose in both classes (e.g.,
m_amountOfOil), then move that member variable to the common base class
and add accessor and mutator methods for it to the base class. So the Robot
base class should have the m_amountOfOil member variable defined once, with
getOil() and addOil()functions, rather than defining this variable directly in both
SmellyRobot and GoofyRobot.
Don’t do this:
class SmellyRobot: public Robot
{
…
private:
int m_oilLeft;
};
class GoofyRobot: public Robot
{
…
private:
int m_oilLeft;
};
Do this instead:
class Robot
{
public:
void addOil(int oil) { m_oilLeft += oil; }
int getOil() const { return m_oilLeft; }
private:
int m_oilLeft;
};
5. Never make any class’s data members public or protected. You may make class
constants public, protected or private.
6. Never make a method public if it is used directly only by other methods within
the same class that holds it. Make it private or protected instead.
7. Your StudentWorld public methods should never return a collection of the
objects StudentWorld maintains or a pointer to such a collection. (Returning a
47
pointer to a single object in the collection is OK.) Only StudentWorld should
know about all of its game objects and where they are. If an action requires
traversing StudentWorld's collection, then a StudentWorld method should do it.
Don’t do this:
class StudentWorld
{
public:
vector<Actor*> getActorsThatCanBeZapped(int x, int y)
{
… // create a vector with a actor pointers and return it
}
};
class NastyRobot
{
public:
virtual void doSomething()
{
…
vector<Actor*> v;
vector<Actor*>::iterator p;
v = studentWorldPtr->getActorsThatCanBeZapped(getX(), getY());
for (p = actors.begin(); p != actors.end(); p++)
p->zap();
}
};
Do this instead:
class StudentWorld
{
public:
void zapAllZappableActors(int x, int y)
{
for (p = actors.begin(); p != actors.end(); p++)
if (p->isAt(x,y) && p->isZappable())
p->zap();
}
};
class NastyRobot
{
public:
virtual void doSomething()
{
…
studentWorldPtr->zapAllZappableActors(getX(), getY());
}
};
48
8. If two subclasses have a method that shares some common functionality, but also
has some differing functionality, use an auxiliary method to factor out the
differences:
Don’t do this:
class StinkyRobot: public Robot
{
…
public:
virtual void doDifferentiatedStuff()
{
doCommonThingA();
passStinkyGas();
pickNose();
doCommonThingB();
}
};
class ShinyRobot: public Robot
{
…
public:
virtual void doDifferentiatedStuff()
{
doCommonThingA();
polishMyChrome();
wipeMyDisplayPanel();
doCommonThingB();
}
};
Do this instead:
class Robot
{
public:
virtual void doSomething()
{
// first do the common thing that all robots do
doCommonThingA();
// then call a virtual function to do the differentiated stuff
doDifferentiatedStuff();
// then do the common final thing that all robots do
doCommonThingB();
}
private:
virtual void doDifferentiatedStuff() = 0;
};
class StinkyRobot: public Robot
{
…
private:
// define StinkyRobot’s version of the differentiated function
49
virtual void doDifferentiatedStuff()
{
// only Stinky robots do these things
passStinkyGas();
pickNose();
}
};
class ShinyRobot: public Robot
{
…
private:
// define ShinyRobot’s version of the differentiated function
virtual void doDifferentiatedStuff()
{
// only Shiny robots do these things
polishMyChrome();
wipeMyDisplayPanel();
}
};
Yes, it is legal for a derived class to override a virtual function that was declared
private in the base class. (It's not trying to use the private member function; it's just
defining a new function.)
Don’t know how or where to start? Read this!
When working on your first large object-oriented program, you’re likely to feel
overwhelmed and have no idea where to start; in fact, it’s likely that many students won’t
be able to finish their entire program. Therefore, it’s important to attack your program
piece by piece rather than trying to program everything at once.
Students who try to program everything at once rather than program incrementally
almost always fail to solve CS32’s project 3, so don’t do it!
Instead, try to get one thing working at a time. Here are some hints:
1. When you define a new class, try to figure out what public member functions it
should have. Then write dummy “stub” code for each of the functions that you’ll fix
later:
class foo
{
public:
int chooseACourseOfAction() { return 0; } // dummy version
};
Try to get your project compiling with these dummy functions first, then you can
worry about filling in the real code later.
50
2. Once you’ve got your program compiling with dummy functions, then start by
replacing one dummy function at a time. Update the function, rebuild your program,
test your new function, and once you’ve got it working, proceed to the next function.
3. Make backups of your working code frequently. Any time you get a new feature
working, make a backup of all your .cpp and .h files just in case you screw
something up later.
BACK UP YOUR .CPP AND .H FILES TO A REMOVABLE DEVICE, ONLINE
STORAGE, OR A PRIVATE GITHUB REPOSITORY EVERY TIME YOU
MAKE A MEANINGFUL CHANGE.
WE WILL NOT ACCEPT EXCUSES THAT YOUR HARD DRIVE/COMPUTER
CRASHED OR THAT YOUR CODE USED TO WORK UNTIL YOU MADE
THAT ONE CHANGE (AND DON’T KNOW WHAT CAUSED IT TO BREAK).
If you use this approach, you’ll always have something working that you can test and
improve upon. If you write everything at once, you’ll end up with hundreds of errors and
just get frustrated! So don’t do it.
Building the Game
The game assets (i.e., image, sound, and level data files) are in a folder named Assets.
The way we’ve written the main routine, your program will look for this folder in a
standard place (described below for Windows and Mac OS X). A few students may find
that their environment is set up in a way that prevents the program from finding the
folder. If that happens to you, change the string literal "Assets" in main.cpp to the full
path name of wherever you choose to put the folder (e.g., "Z:/MarbleMadness/Assets"
or "/Users/fred/MarbleMadness/Assets").
To build the game, follow these steps:
For Windows
Unzip the MarbleMadness-skeleton-windows.zip archive into a folder on your hard drive.
Double-click on MarbleMadness.sln to start Visual Studio.
If you build and run your program from within Visual Studio, the Assets folder should be
in the same folder as your .cpp and .h files. On the other hand, if you launch the program
by double-clicking on the executable file, the Assets folder should be in the same folder
as the executable.
51
For macOS
Unzip the MarbleMadness-skeleton-mac.zip archive into a folder on your hard drive.
Double-click on our provided MarbleMadness.xcodeproj to start Xcode.
If you build and run your program from within Xcode, the Assets directory should be in
the directory yourProjectDir/DerivedData/yourProjectName/BuildProducts/Debug (e.g.,
/Users/fred/MarbleMadness/DerivedData/MarbleMadness/Build/Products/Debug). On
the other hand, if you launch the program by double-clicking on the executable file, the
Assets directory should be in your home directory (e.g., /Users/fred).
What to Turn In
Part #1 (20%)
Ok, so we know you’re scared to death about this project and don’t know where to start.
So, we’re going to incentivize you to work incrementally rather than try to do everything
all at once. For the first part of Project 3, your job is to build a really simple version of
the Marble Madness game that implements maybe 15% of the overall project. You must
program:
1. A class that can serve as the base class for all of your game’s actors (e.g., the
Avatar, RageBots, ThiefBots, goodies, walls, pits, crystals, etc.):
i. It must have a simple constructor.
ii. It must be derived from our GraphObject class.
iii. It must have a member function named doSomething() that can be
called to cause the actor to do something.
iv. You may add other public/private member functions and private data
members to this base class, as you see fit.
2. A wall class, derived in some way from the base class described in 1 above:
i. It must have a simple constructor.
ii. It must have an Image ID of IID_WALL.
iii. It (or its base class) must make itself visible via a call to
setVisible(true);
iv. You may add other public/private member functions and private data
members to your Wall class as you see fit, so long as you use good
object-oriented programming style (e.g., you must NOT duplicate nontrivial functionality across classes).
3. A limited version of your Avatar class, derived in some way from the base
class described in 1 just above (either directly derived from the base class, or
derived from some other class that is somehow derived from the base class):
i. It must have a constructor that initializes the player – see the player
section for more details on where to initialize the player.
52
ii. It must have an Image ID of IID_PLAYER.
iii. It (or its base class) must make itself visible via a call to
setVisible(true);
iv. It must have a limited version of a doSomething() method that lets the
user pick a direction by hitting a directional key. If the player hits a
directional key during the current tick and the target square does not
contain a wall, it updates the player’s location to the target square and
the player’s direction. All this doSomething() method has to do is
properly adjust the player’s x,y coordinates and direction, and our
graphics system will automatically animate its movement it around the
maze!
v. You may add other public/private member functions and private data
members to your Player class as you see fit, so long as you use good
object-oriented programming style (e.g., you must not duplicate
functionality across classes).
4. A limited version of the StudentWorld class.
i. Add any private data members to this class required to keep track of
walls as well as the Avatar object. You may ignore all other items in
the maze such as RageBots, the exit, etc. for part #1.
ii. Implement a constructor for this class that initializes your data
members.
iii. Implement a destructor for this class that frees any remaining
dynamically allocated data that has not yet been freed at the time the
StudentWorld object is destroyed.
iv. Implement the init() method in this class. It must create the player and
insert it into the maze at the right starting location (see the Level class
section of this document for details on the starting location). It must
also create all of the Walls and add them to the maze as specified in
the current Level’s data file.
v. Implement the move() method in your StudentWorld class. During
each tick, it must ask your Avatar and walls to do something. Your
move() method need not check to see if the player has died or not; you
may assume at this point that the player cannot die. Your move()
method does not have to deal with any actors other than the player and
the Walls.
vi. Implement a cleanup() method that frees any dynamically allocated
data that was allocated during calls to the init() method or the move()
method (e.g., it should delete all your allocated Walls and the player).
Note: Your StudentWorld class must have both a destructor and the
cleanUp() method even though they likely do the same thing (in which
case the destructor could just call cleanup()).
As you implement these classes, repeatedly build your program – you’ll probably start
out with lots of errors… Relax and try to remove them and get your program to run.
(Historical note: A UCLA student taking CS131 once got 1,800 compilation errors when
53
compiling a 900-line class project written in the Ada programming language. His name
was Carey Nachenberg. Somehow he survived and has lived a happy life since then.)
Note: Your class names don't have to be exactly the ones we used in this description.
For example, Avatar, PlayerAvatar, Player, or something similar would be reasonable
names for what we called the Avatar class.
You’ll know you’re done with part 1 when your program builds and does the following:
When it runs and the user hits Enter to begin playing, it displays a maze with the player
in its proper starting position. If your classes work properly, you should be able to move
the player around the maze using the directional keys, without the player walking through
any walls.
Your Part #1 solution may actually do more than what is specified above; for example, if
you are further along in the project, and what you have builds and has at least as much
functionality as what’s described above, then you may turn that in instead.
Note, the Part #1 specification above doesn’t require you to implement any RageBots,
ThiefBots, marbles, pits, crystals, any goodies, or the exit (unless you want to). You may
do these unmentioned items if you like but they’re not required for Part 1. However, if
you add additional functionality, make sure that your Avatar, Wall, and
StudentWorld classes still work properly and that your program still builds and
meets the requirements stated above for Part #1!
If you can get this simple version working, you’ll have done a bunch of the hard design
work. You’ll probably still have to change your classes a lot to implement the full
project, but you’ll have done most of the hard thinking.
What to Turn In For Part #1
You must turn in your source code for the simple version of your game, which must
build without errors under either Visual Studio or Xcode. You do not have to get it to
run under more than one compiler. You will turn in a zip file containing nothing more
than these four files:
Actor.h // contains base, Avatar, and Wall class declarations
// as well as constants required by these classes
Actor.cpp // contains the implementation of these classes
StudentWorld.h // contains your StudentWorld class declaration
StudentWorld.cpp // contains your StudentWorld class implementation
You will not be turning in any other files – we’ll test your code with our versions of the
the other .cpp and .h files. Therefore, your solution must NOT modify any of our files or
you will receive zero credit! (Exception: You may modify the string literal "Assets" in
main.cpp.) You will not turn in a report for Part #1; we will not be evaluating Part #1 for
54
program comments, documentation, or test cases; all that matters for Part #1 is correct
behavior for the specified subset of the requirements.
Part #2 (80%)
After you have turned in your work for Part #1 of Project 3, we will discuss one possible
design for this assignment. For the rest of this project, you are welcome to continue to
improve the design that you came up with for Part #1, or you can use the design we
provide.
In Part #2, your goal is to implement a fully working version of the Marble Madness
game, which adheres exactly to the functional specification provided in this document.
What to Turn In For Part #2
You must turn in your source code for your game, which must build without errors
under either Visual Studio or Xcode. You do not have to get it to run under more than
one compiler. You will turn in a zip file containing nothing more than these five files:
Actor.h // contains declarations of your actor classes
// as well as constants required by these classes
Actor.cpp // contains the implementation of these classes
StudentWorld.h // contains your StudentWorld class declaration
StudentWorld.cpp // contains your StudentWorld class implementation
report.docx or report.txt // your report (5% of your grade)
You will not be turning in any other files – we’ll test your code with our versions of the
the other .cpp and .h files. Therefore, your solution must NOT modify any of our files or
you will receive zero credit! (Exception: You may modify the string literal "Assets" or
the value of the constant msPerTick in main.cpp.)
You must turn in a report that contains the following:
1. A description of the control flow for the interaction of the player avatar and a
goodie. Where in the code is the co-location of the two objects detected, and
what happens from that point until the interaction is finished? Which
functions of which objects are called and what do they do during the handling
of this situation?
2. A list of all functionality that you failed to finish as well as known bugs in
your classes, e.g. “I didn’t implement the exit class.” or “My Mean ThiefBot
doesn’t work correctly yet so I treat it like a ThiefBot right now.”
3. A list of other design decisions and assumptions you made; e.g., “It was not
specified what to do in situation X, so this is what I decided to do.”
55
FAQ
Q: Why does my video game run slower/faster than yours?
A: It could be your choice of data structures and algorithms, graphics cards, etc. It’s OK
if your game is faster or a little slower than ours. If your game runs MUCH slower, then
you probably have a Big-O problem and could choose better data structures. If your game
runs faster and you’d like to slow down gameplay, update line 14 in main.cpp with a
larger value, like 20 or 50:
const int msPerTick = 10;
Q: The specification is silent about what to do in a certain situation. What should I do?
A: Play with our sample program and do what it does. Use our program as a reference.
If neither the specification nor our program makes it clear what to do, do whatever seems
reasonable and document it in your report. If the specification is unclear, but your
program behaves like our demonstration program, YOU WILL NOT LOSE
POINTS!
Q: What should I do if I can’t finish the project?!
A: Do as much as you can, and whatever you do, make sure your code builds! If we can
sort of play your game, but it’s not complete or perfect, that’s better than it not even
building!
Q: Where can I go for help?
A: Try TBP/HKN/UPE – they provide free tutoring and can help your with your project!
Q: Can I work with my classmates on this?
A: You can discuss general ideas about the project, but don’t share source code with your
classmates.
GOOD LUCK!
版权所有:编程辅导网 2021 All Rights Reserved 联系方式:QQ:99515681 微信:codinghelp 电子信箱:99515681@qq.com
免责声明:本站部分内容从网络整理而来,只供参考!如有版权问题可联系本站删除。