summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/dataTests.cpp184
-rw-r--r--tests/logicTests.cpp685
-rw-r--r--tests/presentationTests.cpp248
3 files changed, 1117 insertions, 0 deletions
diff --git a/tests/dataTests.cpp b/tests/dataTests.cpp
new file mode 100644
index 0000000..6bf553e
--- /dev/null
+++ b/tests/dataTests.cpp
@@ -0,0 +1,184 @@
+/** @file dataTests.cpp
+* @brief Unit tests for the data layer of a Rally-X game.
+*
+* The functionality of each class in the data layer was tested.
+*
+* The Config class was tested in terms of its ability to read an existing config file,
+* as well as its ability to create a new file with default values, or fill missing parameters
+* with default values.
+*
+* The LevelReader class was tested in the normal case, where a correct level file is given,
+* and in the case where a file that does not exist is given.
+*
+* @author Justin Wernick
+* @author David Schneider
+*/
+
+#include <cstdio>
+#include <fstream>
+using namespace std;
+
+#include <gtest/gtest.h>
+
+#include "../source/data/Config.h"
+#include "../source/data/LevelReader.h"
+#include "../source/logic/PlayerCar.h"
+#include "../source/logic/EnemyCar.h"
+#include "../source/logic/Checkpoint.h"
+#include "../source/logic/Rock.h"
+#include "../source/logic/Smokescreen.h"
+#include "../source/logic/Maze.h"
+
+/**
+* @brief Tests that a normal complete config file can be read.
+*/
+TEST(Config, readsSettingsCorrectly)
+{
+ string testFilepath = "testConfig.txt";
+ ofstream testFile(testFilepath.c_str());
+
+ testFile << "screen_width=123" << endl;
+ testFile << "screen_height=345" << endl;
+ testFile << "fullscreen=true" << endl;
+
+ testFile.close();
+
+ Config testConfig(testFilepath);
+
+ EXPECT_EQ((unsigned)(123), testConfig.screenWidth());
+ EXPECT_EQ((unsigned)(345), testConfig.screenHeight());
+ EXPECT_TRUE(testConfig.fullscreen());
+
+ remove(testFilepath.c_str());
+}
+
+/**
+* @brief Tests that, if the config file does not exist, it is created with default values.
+*/
+TEST(Config, createsFileIfNeeded)
+{
+ string testFilepath = "testConfig.txt";
+ Config testConfig(testFilepath);
+
+ ifstream testFile(testFilepath.c_str());
+ EXPECT_TRUE(testFile);
+ testFile.close();
+
+ //test for default values
+ EXPECT_EQ((unsigned)(800), testConfig.screenWidth());
+ EXPECT_EQ((unsigned)(600), testConfig.screenHeight());
+ EXPECT_FALSE(testConfig.fullscreen());
+
+ remove(testFilepath.c_str());
+}
+
+/**
+* @brief Tests that an incomplete config file is loaded, with defaults for the missing values.
+*/
+TEST(Config, incompleteFileFilled)
+{
+ string testFilepath = "testConfig.txt";
+ ofstream testFile(testFilepath.c_str());
+
+ testFile << "screen_height=345" << endl;
+
+ testFile.close();
+
+ Config testConfig(testFilepath);
+
+ EXPECT_EQ((unsigned)(800), testConfig.screenWidth());
+ EXPECT_EQ((unsigned)(345), testConfig.screenHeight());
+ EXPECT_FALSE(testConfig.fullscreen());
+
+ remove(testFilepath.c_str());
+}
+
+/**
+* @brief Tests that a level can be loaded correctly from a file.
+*/
+TEST(LevelReader, readsFileInfoObjects)
+{
+ string testFilepath = "testMaze.lvl";
+ ofstream testFile(testFilepath.c_str());
+
+ testFile << " P X " << endl;
+ testFile << " " << endl;
+ testFile << " @ " << endl;
+ testFile << " X" << endl;
+ testFile << "## " << endl;
+ testFile << " P " << endl;
+ testFile << " O " << endl;
+
+ testFile.close();
+
+ LevelReader testReader(testFilepath);
+ Maze maze;
+ list<PlayerCar> players;
+ list<EnemyCar> enemies;
+ list<Checkpoint> checkpoints;
+ list<Rock> rocks;
+ testReader.readLevel(maze, players, enemies, checkpoints, rocks);
+
+ list<PlayerCar> expectedPlayers;
+ list<EnemyCar> expectedEnemies;
+ list<Checkpoint> expectedCheckpoints;
+ list<Rock> expectedRocks;
+
+ expectedPlayers.push_back(PlayerCar(3,2));
+ expectedEnemies.push_back(EnemyCar(3,0));
+ expectedEnemies.push_back(EnemyCar(5,3));
+ expectedCheckpoints.push_back(Checkpoint(2,0));
+ expectedCheckpoints.push_back(Checkpoint(3,5));
+ expectedRocks.push_back(Rock(2,6));
+
+ //eqality operator was not implemented for the GameObject class or its subclasses
+ //because it would not be meaningful. Two objects with the same position, type, and facing
+ //are still two different objects.
+ //iterators were used because the list type does not have an 'at' function.
+ list<PlayerCar>::const_iterator playIter = players.begin();
+ EXPECT_FLOAT_EQ(playIter->x(), 3);
+ EXPECT_FLOAT_EQ(playIter->y(), 2);
+ ++playIter;
+ EXPECT_EQ(playIter, players.end());
+
+ list<EnemyCar>::const_iterator enemyIter = enemies.begin();
+ EXPECT_FLOAT_EQ(enemyIter->x(), 3);
+ EXPECT_FLOAT_EQ(enemyIter->y(), 0);
+ ++enemyIter;
+ EXPECT_FLOAT_EQ(enemyIter->x(), 5);
+ EXPECT_FLOAT_EQ(enemyIter->y(), 3);
+ ++enemyIter;
+ EXPECT_EQ(enemyIter, enemies.end());
+
+ list<Checkpoint>::const_iterator checkIter = checkpoints.begin();
+ EXPECT_FLOAT_EQ(checkIter->x(), 1);
+ EXPECT_FLOAT_EQ(checkIter->y(), 0);
+ ++checkIter;
+ EXPECT_FLOAT_EQ(checkIter->x(), 3);
+ EXPECT_FLOAT_EQ(checkIter->y(), 5);
+ ++checkIter;
+ EXPECT_EQ(checkIter, checkpoints.end());
+
+ list<Rock>::const_iterator rockIter = rocks.begin();
+ EXPECT_FLOAT_EQ(rockIter->x(), 2);
+ EXPECT_FLOAT_EQ(rockIter->y(), 6);
+ ++rockIter;
+ EXPECT_EQ(rockIter, rocks.end());
+
+ remove(testFilepath.c_str());
+}
+
+/**
+* @brief Tests that an exception is throws if the selected file does not exist.
+*/
+TEST(LevelReader, throwsExceptionOnBadFilename)
+{
+ string testFilepath = "testLevel.lvl";
+ LevelReader testReader(testFilepath);
+ Maze maze;
+ list<PlayerCar> players;
+ list<EnemyCar> enemies;
+ list<Checkpoint> checkpoints;
+ list<Rock> rocks;
+ EXPECT_ANY_THROW(testReader.readLevel(maze, players, enemies, checkpoints, rocks));
+}
diff --git a/tests/logicTests.cpp b/tests/logicTests.cpp
new file mode 100644
index 0000000..97d9ed9
--- /dev/null
+++ b/tests/logicTests.cpp
@@ -0,0 +1,685 @@
+/** @file logicTests.cpp
+* @brief Unit tests for the logic layer of a Rally-X game.
+*
+* The classes used to run Allegro's install and uninstall functions were tested indirectly
+* through the use of other classes. If the AllegroWrappers classes are not working, they will
+* result in tests failing, or the program crashing.
+*
+* The Car class's movement is tested through implementing a subclass of Car, PlayerCar, facing
+* in a given direction with and without maze walls in the way, and testing its position after
+* a frame has passed.
+*
+* The counting system with the Checkpoint class is tested through repeated creation and
+* destruction of Checkpoint objects.
+*
+* The CollisionDetecter class is tested by setting up situations with each type of collision,
+* and checking that they have the desired results on the objects involved.
+*
+* The DestroyedObjectPopup class is tested to ensure that it lasts the desired amount of time
+* before being destroyed.
+*
+* The intelligence of the EnemyCar class is not unit tested, as it may change as extra states
+* and AI schemes are implemented, possibly including a random element. Therefore, it should be
+* tested manually by playing example levels and monitoring if the enemies appear to behave in
+* a manner that makes gameplay interesting.
+*
+* The Game class is not tested directly. It pulls all of the units being tested together into
+* a complete game, and so should be tested by running the Game.
+*
+* The GameObject class is tested indirectly through testing its subclasses.
+*
+* The LimitedTimeObject class is tested indirectly through the DestroyedObjectPopup and Smokescreen classes.
+*
+* The Maze class is tested in terms of its ability to construct itself correctly from a list of
+* coordinates. It is also tested with the PlayerCar class where the movement of the PlayerCar is
+* tested in a Maze.
+*
+* The MazeMath class is tested by running each of its functions with expected input and output values.
+*
+* The PlayerCar class is tested on its ability to move correctly as part of the tests for the Car class.
+* The functioning of the PlayerCar's petrol system is also tested. Controlling of the PlayerCar
+* is not tested, and should be tested manually by running the game and verifying that the arrow keys
+* cause the direction of the PlayerCar to change appropriately. Further, the creation of Smokescreens
+* by pressing the spacebar should also be tested manually.
+*
+* The Rock class only has functionality in collisions, and so is tested through the CollisionDetector's
+* tests.
+*
+* The Smokescreen class is tested in that it disappears after a set time. Its affect on the EnemyCar class
+* is tested through the CollisionDetector's tests.
+*
+* @author Justin Wernick
+* @author David Schneider
+*/
+
+#include <vector>
+#include <list>
+#include <utility>
+using namespace std;
+
+#include <gtest/gtest.h>
+
+#include "../source/logic/CollisionDetector.h"
+#include "../source/logic/PlayerCar.h"
+#include "../source/logic/EnemyCar.h"
+#include "../source/logic/Checkpoint.h"
+#include "../source/logic/Rock.h"
+#include "../source/logic/Smokescreen.h"
+#include "../source/logic/Maze.h"
+#include "../source/logic/MazeMath.h"
+#include "../source/logic/DestroyedObjectPopup.h"
+
+/**
+* @brief Tests that PlayerCar moves the correct amount in the up direction on each frame without obstacles.
+*/
+TEST(Car, updateMovesPlayerUpInEmptyMaze)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(2,2,Maze::UP);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 2;
+ double expectY = 2-player.speed();
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+
+/**
+* @brief Tests that PlayerCar moves the correct amount in the down direction on each frame without obstacles.
+*/
+TEST(Car, updateMovesPlayerDownInEmptyMaze)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(2,2,Maze::DOWN);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 2;
+ double expectY = 2+player.speed();
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+
+/**
+* @brief Tests that PlayerCar moves the correct amount in the left direction on each frame without obstacles.
+*/
+TEST(Car, updateMovesPlayerLeftInEmptyMaze)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(2,2,Maze::LEFT);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 2-player.speed();
+ double expectY = 2;
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+
+/**
+* @brief Tests that PlayerCar moves the correct amount in the right direction on each frame without obstacles.
+*/
+TEST(Car, updateMovesPlayerRightInEmptyMaze)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(2,2,Maze::RIGHT);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 2+player.speed();
+ double expectY = 2;
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+
+/**
+* @brief Tests that PlayerCar does not move up if the path is blocked.
+*/
+TEST(Car, carNotMovedWhenPathUpBlocked)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ walls.push_back(make_pair(3,3));
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(3,4,Maze::UP);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 3;
+ double expectY = 4;
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+/**
+* @brief Tests that PlayerCar does not move down if the path is blocked.
+*/
+TEST(Car, carNotMovedWhenPathDownBlocked)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ walls.push_back(make_pair(3,3));
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(3,2,Maze::DOWN);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 3;
+ double expectY = 2;
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+/**
+* @brief Tests that PlayerCar does not move left if the path is blocked.
+*/
+TEST(Car, carNotMovedWhenPathLeftBlocked)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ walls.push_back(make_pair(3,3));
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(4,3,Maze::LEFT);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 4;
+ double expectY = 3;
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+/**
+* @brief Tests that PlayerCar does not move right if the path is blocked.
+*/
+TEST(Car, carNotMovedWhenPathRightBlocked)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ walls.push_back(make_pair(3,3));
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(2,3,Maze::RIGHT);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 2;
+ double expectY = 3;
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+
+/**
+* @brief Tests that PlayerCar does not move up if currently on the top row of the maze.
+*/
+TEST(Car, carDoesNotMoveUpOutMaze)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(3,0,Maze::UP);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 3;
+ double expectY = 0;
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+/**
+* @brief Tests that PlayerCar does not move down if currently on the bottom row of the maze.
+*/
+TEST(Car, carDoesNotMoveDownOutMaze)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(3,5,Maze::DOWN);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 3;
+ double expectY = 5;
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+/**
+* @brief Tests that PlayerCar does not move left if currently on the first column of the maze.
+*/
+TEST(Car, carDoesNotMoveLeftOutMaze)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(0,3,Maze::LEFT);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 0;
+ double expectY = 3;
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+/**
+* @brief Tests that PlayerCar does not move right if currently on the last column of the maze.
+*/
+TEST(Car, carDoesNotMoveRightOutMaze)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(5,3,Maze::RIGHT);
+
+ list<Smokescreen> smokescreens;
+ player.update(testMaze, smokescreens);
+
+ double expectX = 5;
+ double expectY = 3;
+ EXPECT_FLOAT_EQ(expectX, player.x());
+ EXPECT_FLOAT_EQ(expectY, player.y());
+}
+
+/**
+* @brief Tests that counting of the number of checkpoints happens correctly.
+*/
+TEST(Checkpoint, countIncrementsAndDecrements)
+{
+ vector<Checkpoint> checkpoints;
+ EXPECT_EQ(0, Checkpoint::checkpointCount());
+
+ for (int i=0; i<1000; ++i)
+ {
+ checkpoints.push_back(Checkpoint(i,i));
+ }
+ EXPECT_EQ(1000, Checkpoint::checkpointCount());
+
+ //brackets to limit scope
+ {
+ Checkpoint extraCheck1(0,0);
+ Checkpoint extraCheck2 = extraCheck1;
+ EXPECT_EQ(1002, Checkpoint::checkpointCount());
+ extraCheck2 = extraCheck1;
+ EXPECT_EQ(1002, Checkpoint::checkpointCount());
+ }
+ EXPECT_EQ(1000, Checkpoint::checkpointCount());
+
+ while(!checkpoints.empty())
+ {
+ checkpoints.pop_back();
+ }
+ EXPECT_EQ(0, Checkpoint::checkpointCount());
+}
+
+/**
+* @brief Tests that when a player and enemy overlap, both are marked for destruction.
+*/
+TEST(CollisionDetector, playerAndEnemyBothDestroyed)
+{
+ list<PlayerCar> players;
+ list<EnemyCar> enemies;
+ list<Checkpoint> checkpoints;
+ list<Rock> rocks;
+ list<Smokescreen> smokescreens;
+ CollisionDetector detector;
+
+ players.push_back(PlayerCar(5,5));
+ players.push_back(PlayerCar(5,6));
+ players.push_back(PlayerCar(6,5));
+ players.push_back(PlayerCar(7,7.5));
+
+ enemies.push_back(EnemyCar(5,6));
+ enemies.push_back(EnemyCar(4,5));
+ enemies.push_back(EnemyCar(6,6));
+ enemies.push_back(EnemyCar(6.5,8));
+
+ detector.checkCollisions(players, enemies, checkpoints, rocks, smokescreens);
+
+ list<PlayerCar>::const_iterator playerIter = players.begin();
+ EXPECT_FALSE(playerIter->destroyed());
+ ++playerIter;
+ EXPECT_TRUE(playerIter->destroyed());
+ ++playerIter;
+ EXPECT_FALSE(playerIter->destroyed());
+ ++playerIter;
+ EXPECT_TRUE(playerIter->destroyed());
+
+ list<EnemyCar>::const_iterator enemyIter = enemies.begin();
+ EXPECT_TRUE(enemyIter->destroyed());
+ ++enemyIter;
+ EXPECT_FALSE(enemyIter->destroyed());
+ ++enemyIter;
+ EXPECT_FALSE(enemyIter->destroyed());
+ ++enemyIter;
+ EXPECT_TRUE(enemyIter->destroyed());
+}
+
+/**
+* @brief Tests that when a player and rock overlap, only the player is marked for destruction.
+*/
+TEST(CollisionDetector, playerDestroyedByRock)
+{
+ list<PlayerCar> players;
+ list<EnemyCar> enemies;
+ list<Checkpoint> checkpoints;
+ list<Rock> rocks;
+ list<Smokescreen> smokescreens;
+ CollisionDetector detector;
+
+ players.push_back(PlayerCar(5,5));
+ players.push_back(PlayerCar(5,6));
+ players.push_back(PlayerCar(6,5));
+ players.push_back(PlayerCar(7,7.5));
+
+ rocks.push_back(Rock(5,6));
+ rocks.push_back(Rock(4,5));
+ rocks.push_back(Rock(6,6));
+ rocks.push_back(Rock(6.5,8));
+
+ detector.checkCollisions(players, enemies, checkpoints, rocks, smokescreens);
+
+ list<PlayerCar>::const_iterator playerIter = players.begin();
+ EXPECT_FALSE(playerIter->destroyed());
+ ++playerIter;
+ EXPECT_TRUE(playerIter->destroyed());
+ ++playerIter;
+ EXPECT_FALSE(playerIter->destroyed());
+ ++playerIter;
+ EXPECT_TRUE(playerIter->destroyed());
+
+ list<Rock>::const_iterator rockIter = rocks.begin();
+ EXPECT_FALSE(rockIter->destroyed());
+ ++rockIter;
+ EXPECT_FALSE(rockIter->destroyed());
+ ++rockIter;
+ EXPECT_FALSE(rockIter->destroyed());
+ ++rockIter;
+ EXPECT_FALSE(rockIter->destroyed());
+}
+
+/**
+* @brief Tests that when a player and checkpoint overlap, only the checkpoint is marked for destruction.
+*/
+TEST(CollisionDetector, checkpointDestroyedByPlayer)
+{
+ list<PlayerCar> players;
+ list<EnemyCar> enemies;
+ list<Checkpoint> checkpoints;
+ list<Rock> rocks;
+ list<Smokescreen> smokescreens;
+ CollisionDetector detector;
+
+ players.push_back(PlayerCar(5,5));
+ players.push_back(PlayerCar(5,6));
+ players.push_back(PlayerCar(6,5));
+ players.push_back(PlayerCar(7,7.5));
+
+ checkpoints.push_back(Checkpoint(5,6));
+ checkpoints.push_back(Checkpoint(4,5));
+ checkpoints.push_back(Checkpoint(6,6));
+ checkpoints.push_back(Checkpoint(6.5,8));
+
+ detector.checkCollisions(players, enemies, checkpoints, rocks, smokescreens);
+
+ list<PlayerCar>::const_iterator playerIter = players.begin();
+ EXPECT_FALSE(playerIter->destroyed());
+ ++playerIter;
+ EXPECT_FALSE(playerIter->destroyed());
+ ++playerIter;
+ EXPECT_FALSE(playerIter->destroyed());
+ ++playerIter;
+ EXPECT_FALSE(playerIter->destroyed());
+
+ list<Checkpoint>::const_iterator checkpointIter = checkpoints.begin();
+ EXPECT_TRUE(checkpointIter->destroyed());
+ ++checkpointIter;
+ EXPECT_FALSE(checkpointIter->destroyed());
+ ++checkpointIter;
+ EXPECT_FALSE(checkpointIter->destroyed());
+ ++checkpointIter;
+ EXPECT_TRUE(checkpointIter->destroyed());
+}
+
+/**
+* @brief Tests that when an enemy and smokescreen overlap, the enemy's speed becomes zero.
+*/
+TEST(CollisionDetector, enemyStoppedBySmokescreen)
+{
+ list<PlayerCar> players;
+ list<EnemyCar> enemies;
+ list<Checkpoint> checkpoints;
+ list<Rock> rocks;
+ list<Smokescreen> smokescreens;
+ CollisionDetector detector;
+
+ enemies.push_back(EnemyCar(5,5));
+ enemies.push_back(EnemyCar(5,6));
+ enemies.push_back(EnemyCar(6,5));
+ enemies.push_back(EnemyCar(7,7.5));
+
+ smokescreens.push_back(Smokescreen(5,6));
+ smokescreens.push_back(Smokescreen(4,5));
+ smokescreens.push_back(Smokescreen(6,6));
+ smokescreens.push_back(Smokescreen(6.5,8));
+
+ detector.checkCollisions(players, enemies, checkpoints, rocks, smokescreens);
+
+ list<EnemyCar>::const_iterator enemyIter = enemies.begin();
+ EXPECT_GT(enemyIter->speed(),0);
+ ++enemyIter;
+ EXPECT_FLOAT_EQ(0, enemyIter->speed());
+ ++enemyIter;
+ EXPECT_GT(enemyIter->speed(),0);
+ ++enemyIter;
+ EXPECT_FLOAT_EQ(0, enemyIter->speed());
+}
+
+/**
+* @brief Tests that a DestroyedObjectPopup is destroyed 30 frames after it is created.
+*/
+TEST(DestroyedObjectPopup, destroyedAfterSetTime)
+{
+ DestroyedObjectPopup testPopup(3,2,BitmapStore::CRASHED_CAR);
+
+ int i=0;
+ while (!testPopup.destroyed())
+ {
+ testPopup.update();
+ ++i;
+ }
+
+ EXPECT_EQ(30,i);
+}
+
+/**
+* @brief Tests that the maze treats the undefined area outside its bounds as being walls.
+*/
+TEST(Maze, queryOutsideBoundsReturnsSolid)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ testMaze.generateMaze(walls,5,5);
+
+ EXPECT_FALSE(testMaze.getSolid(5,5));
+ EXPECT_FALSE(testMaze.getSolid(0,0));
+ EXPECT_TRUE(testMaze.getSolid(6,5));
+ EXPECT_TRUE(testMaze.getSolid(5,6));
+ EXPECT_TRUE(testMaze.getSolid(-1,3));
+ EXPECT_TRUE(testMaze.getSolid(3,-1));
+}
+
+/**
+* @brief Tests that a maze can be generated without extending the width and height for objects.
+*/
+TEST(Maze, generationWithWallsWithoutObjectMax)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ walls.push_back(make_pair(1,2));
+ walls.push_back(make_pair(4,3));
+ testMaze.generateMaze(walls);
+
+ //test random empty blocks
+ EXPECT_FALSE(testMaze.getSolid(2,1));
+ EXPECT_FALSE(testMaze.getSolid(0,0));
+
+ //test wall blocks
+ EXPECT_TRUE(testMaze.getSolid(1,2));
+ EXPECT_TRUE(testMaze.getSolid(4,3));
+
+ //test bounds are being set right
+ EXPECT_TRUE(testMaze.getSolid(4,4));
+ EXPECT_TRUE(testMaze.getSolid(5,3));
+}
+
+/**
+* @brief Tests that a maze can be generated with extending the width and height for objects.
+*/
+TEST(Maze, generationWithWallsWithObjectMax)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ walls.push_back(make_pair(1,2));
+ walls.push_back(make_pair(4,3));
+ testMaze.generateMaze(walls,5,6);
+
+ //test random empty blocks
+ EXPECT_FALSE(testMaze.getSolid(2,1));
+ EXPECT_FALSE(testMaze.getSolid(0,0));
+
+ //test wall blocks
+ EXPECT_TRUE(testMaze.getSolid(1,2));
+ EXPECT_TRUE(testMaze.getSolid(4,3));
+
+ //test bounds are being set right
+ EXPECT_FALSE(testMaze.getSolid(4,4));
+ EXPECT_FALSE(testMaze.getSolid(5,6));
+ EXPECT_TRUE(testMaze.getSolid(6,6));
+ EXPECT_TRUE(testMaze.getSolid(5,7));
+}
+
+/**
+* @brief Tests that the formula for a straight line distance works as expected.
+*/
+TEST(MazeMath, distanceGivesExpectedResult)
+{
+ //right angle triange with sides of length 3, 4, and 5
+ double x1 = 1;
+ double y1 = 1;
+ double x2 = 4;
+ double y2 = 5;
+
+ double expectedResult = 5;
+
+ EXPECT_DOUBLE_EQ(expectedResult, MazeMath::distance(x1,y1,x2,y2));
+}
+
+/**
+* @brief Tests that rounding off function works as expected.
+*/
+TEST(MazeMath, roundGivesExpectedResult)
+{
+ //right angle triange with sides of length 3, 4, and 5
+ double roundUp = 5.5;
+ double roundDown = 5.49;
+
+ double expectUp = 6;
+ double expectDown = 5;
+
+ EXPECT_DOUBLE_EQ(expectUp, MazeMath::round(roundUp));
+ EXPECT_DOUBLE_EQ(expectDown, MazeMath::round(roundDown));
+}
+
+/**
+* @brief Tests that a PlayerCar can be created and destroyed repeatedly without incident.
+*
+* This tests that the PlayerCar can handle its own dependencies on Allegro, even when
+* copy constructors are used (through vector).
+*/
+TEST(PlayerCar, creationAndDestructionCanHappen)
+{
+ EXPECT_NO_THROW({
+ vector<PlayerCar> players;
+ for (int i=0; i<10; ++i)
+ {
+ players.push_back(PlayerCar(i,i));
+ }
+ while(!players.empty())
+ {
+ players.pop_back();
+ }
+ });
+
+ EXPECT_FALSE(al_is_system_installed());
+}
+
+/**
+* @brief Tests that the player's petrol runs out, and that the player's speed is halved after that.
+*
+* Also tests that the speed is recovered if petrol is increased again.
+*/
+TEST(PlayerCar, playerSpeedAffectedByPetrol)
+{
+ Maze testMaze;
+ vector<pair<int,int> > walls;
+ testMaze.generateMaze(walls,5,5);
+
+ PlayerCar player(5,3,Maze::RIGHT);
+
+ list<Smokescreen> smokescreens;
+ for (int i=0; i<1429; ++i)
+ {
+ player.update(testMaze, smokescreens);
+ }
+
+ EXPECT_FLOAT_EQ(0, player.petrol());
+ EXPECT_FLOAT_EQ(0.05, player.speed());
+
+ player.gotCheckpoint();
+ EXPECT_GT(player.petrol(), 0);
+ EXPECT_FLOAT_EQ(0.1, player.speed());
+}
+
+/**
+* @brief Tests that a Smokescreen is destroyed 60 frames after it is created.
+*/
+TEST(Smokescreen, destroyedAfterSetTime)
+{
+ Smokescreen testSmokescreen(3,2);
+
+ int i=0;
+ while (!testSmokescreen.destroyed())
+ {
+ testSmokescreen.update();
+ ++i;
+ }
+
+ EXPECT_EQ(60,i);
+}
diff --git a/tests/presentationTests.cpp b/tests/presentationTests.cpp
new file mode 100644
index 0000000..d07ac99
--- /dev/null
+++ b/tests/presentationTests.cpp
@@ -0,0 +1,248 @@
+/** @file presentationTests.cpp
+* @brief Unit tests for the presentation layer of a Rally-X game.
+*
+* The BitmapStore class is tested in its ability to return a bitmap for
+* each image. The appearance of the bitmaps need to be tested manually
+* by running the game and inspecting the various objects.
+*
+* The ColourStore class is tested in its ability to return a colour for
+* each image. The appearance of the colours need to be tested manually
+* by running the game and inspecting the various objects.
+*
+* The GamePanel and InfoPanel classes depend on their visual appearance in
+* the game. Therefore, they should be tested manually. The technical part
+* of their functionality, creating the back and front buffers and changing
+* between them, is handled by their superclass, ScreenPanel.
+*
+* The KeyboardHandler depends on user inputs. It should be tested manually
+* by testing that the player's direction can be controlled, and that smokescreens
+* can be created.
+*
+* The Screen class's creation with various resolutions and fullscreen settings is
+* tested. An exception should be thrown if the fullscreen resolution is not supported
+* by the current hardware, but not otherwise. The visual appearance and creation of
+* ScreenPanels should be tested manually by running the game.
+*
+* The ScreenPanel class is tested by creating a false back and front display buffer. The buffer
+* that is being drawn to is tested by sampling a pixel in the middle of the buffer.
+*
+* @author Justin Wernick
+* @author David Schneider
+*/
+
+#include <utility>
+#include <list>
+using namespace std;
+
+#include <gtest/gtest.h>
+
+#include "../source/presentation/BitmapStore.h"
+#include "../source/presentation/ColourStore.h"
+#include "../source/presentation/GamePanel.h"
+#include "../source/presentation/Screen.h"
+
+#include "../source/logic/PlayerCar.h"
+#include "../source/logic/EnemyCar.h"
+#include "../source/logic/Checkpoint.h"
+#include "../source/logic/Rock.h"
+#include "../source/logic/Smokescreen.h"
+#include "../source/logic/Maze.h"
+#include "../source/logic/DestroyedObjectPopup.h"
+
+/**
+* @brief Tests that all images can be requested without failure occuring.
+*/
+TEST(BitmapStore, returnsBitmapForAllImages)
+{
+ BitmapStore testStore(50);
+
+ ALLEGRO_BITMAP* testBitmap = NULL;
+
+ EXPECT_NO_FATAL_FAILURE(testBitmap = testStore.getBitmap(BitmapStore::PLAYER));
+ EXPECT_NO_FATAL_FAILURE(testBitmap = testStore.getBitmap(BitmapStore::ENEMY));
+ EXPECT_NO_FATAL_FAILURE(testBitmap = testStore.getBitmap(BitmapStore::CHECKPOINT));
+ EXPECT_NO_FATAL_FAILURE(testBitmap = testStore.getBitmap(BitmapStore::ROCK));
+ EXPECT_NO_FATAL_FAILURE(testBitmap = testStore.getBitmap(BitmapStore::MAZE_WALL));
+ EXPECT_NO_FATAL_FAILURE(testBitmap = testStore.getBitmap(BitmapStore::MAZE_FLOOR));
+ EXPECT_NO_FATAL_FAILURE(testBitmap = testStore.getBitmap(BitmapStore::SMOKE));
+ EXPECT_NO_FATAL_FAILURE(testBitmap = testStore.getBitmap(BitmapStore::CRASHED_CAR));
+ EXPECT_NO_FATAL_FAILURE(testBitmap = testStore.getBitmap(BitmapStore::CLAIMED_CHECKPOINT));
+}
+
+/**
+* @brief Tests that all colours can be requested without failure occuring.
+*/
+TEST(ColourStore, returnsColourForAllImages)
+{
+ ColourStore testStore;
+
+ ALLEGRO_COLOR testColour;
+
+ EXPECT_NO_FATAL_FAILURE(testColour = testStore.getColour(BitmapStore::PLAYER));
+ EXPECT_NO_FATAL_FAILURE(testColour = testStore.getColour(BitmapStore::ENEMY));
+ EXPECT_NO_FATAL_FAILURE(testColour = testStore.getColour(BitmapStore::CHECKPOINT));
+ EXPECT_NO_FATAL_FAILURE(testColour = testStore.getColour(BitmapStore::ROCK));
+ EXPECT_NO_FATAL_FAILURE(testColour = testStore.getColour(BitmapStore::MAZE_WALL));
+ EXPECT_NO_FATAL_FAILURE(testColour = testStore.getColour(BitmapStore::MAZE_FLOOR));
+ EXPECT_NO_FATAL_FAILURE(testColour = testStore.getColour(BitmapStore::SMOKE));
+ EXPECT_NO_FATAL_FAILURE(testColour = testStore.getColour(BitmapStore::CRASHED_CAR));
+ EXPECT_NO_FATAL_FAILURE(testColour = testStore.getColour(BitmapStore::CLAIMED_CHECKPOINT));
+}
+
+/**
+* Tests that an exception is thrown if fullscreen mode is requested on an unsupported monitor resolution.
+*/
+TEST(Screen, exceptionOnBadResolution)
+{
+ //resolution should be unsupported on most displays
+ int badWidth = 1000;
+ int badHeight = 5;
+
+ EXPECT_ANY_THROW(Screen(badWidth, badHeight, true));
+}
+/**
+* Tests that an exception is not thrown if windowed mode is requested on an unsupported monitor resolution.
+*/
+TEST(Screen, noExceptionOnWindowed)
+{
+ int badWidth = 1000;
+ int badHeight = 5;
+
+ EXPECT_NO_THROW(Screen(badWidth, badHeight, false));
+
+}
+/**
+* Tests that an exception is not thrown if fullscreen mode is requested on a supported monitor resolution.
+*/
+TEST(Screen, noExceptionOnGoodResolution)
+{
+ //resolution should be supported on most monitors
+ int goodWidth = 800;
+ int goodHeight = 600;
+
+ EXPECT_NO_THROW(Screen(goodWidth, goodHeight, true));
+}
+
+/**
+* @brief Tests that when a ScreenPanel draws to the back buffer provided.
+*/
+TEST(ScreenPanel, drawingToCurrentBackBuffer)
+{
+ al_init();
+
+ ALLEGRO_BITMAP* testBitmapBack = al_create_bitmap(500,500);
+ ALLEGRO_BITMAP* testBitmapFront = al_create_bitmap(500,500);
+
+ ALLEGRO_COLOR blankColour = al_map_rgb(0,0,0);
+ al_set_target_bitmap(testBitmapBack);
+ al_clear_to_color(blankColour);
+ al_set_target_bitmap(testBitmapFront);
+ al_clear_to_color(blankColour);
+
+ GamePanel testPanel(testBitmapBack, testBitmapFront, 0, 0, 500, 500);
+ Maze testMaze;
+
+ vector<pair<int,int> > wallsFull;
+
+ for (int x=0; x<20; ++x)
+ {
+ for (int y=0; y<20; ++y)
+ {
+ wallsFull.push_back(make_pair(x,y));
+ }
+ }
+ testMaze.generateMaze(wallsFull,20,20);
+
+ list<PlayerCar> players;
+ list<EnemyCar> enemies;
+ list<Checkpoint> checkpoints;
+ list<Rock> rocks;
+ list<Smokescreen> smokescreens;
+ list<DestroyedObjectPopup> popups;
+
+ testPanel.draw(testMaze, players, enemies, checkpoints, rocks, smokescreens, popups);
+
+ BitmapStore bitmapStore(50);
+ ALLEGRO_BITMAP* wall = bitmapStore.getBitmap(BitmapStore::MAZE_WALL);
+ ALLEGRO_COLOR mazeColour = al_get_pixel(wall, 25, 25);
+
+ ALLEGRO_COLOR backSample = al_get_pixel(testBitmapBack, 250, 250);
+ ALLEGRO_COLOR frontSample = al_get_pixel(testBitmapFront, 250, 250);
+
+ EXPECT_FLOAT_EQ(mazeColour.r, backSample.r);
+ EXPECT_FLOAT_EQ(mazeColour.g, backSample.g);
+ EXPECT_FLOAT_EQ(mazeColour.b, backSample.b);
+ EXPECT_FLOAT_EQ(mazeColour.a, backSample.a);
+
+ EXPECT_FLOAT_EQ(blankColour.r, frontSample.r);
+ EXPECT_FLOAT_EQ(blankColour.g, frontSample.g);
+ EXPECT_FLOAT_EQ(blankColour.b, frontSample.b);
+ EXPECT_FLOAT_EQ(blankColour.a, frontSample.a);
+
+ al_destroy_bitmap(testBitmapBack);
+ al_destroy_bitmap(testBitmapFront);
+}
+
+/**
+* @brief Tests that when a ScreenPanel draws to the front buffer provided after a flip has been called.
+*/
+TEST(ScreenPanel, drawingToCurrentBackBufferAfterFlip)
+{
+ al_init();
+
+ al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP); //removes dependency on display existing
+
+ ALLEGRO_BITMAP* testBitmapBack = al_create_bitmap(500,500);
+ ALLEGRO_BITMAP* testBitmapFront = al_create_bitmap(500,500);
+
+ ALLEGRO_COLOR blankColour = al_map_rgb(0,0,0);
+ al_set_target_bitmap(testBitmapBack);
+ al_clear_to_color(blankColour);
+ al_set_target_bitmap(testBitmapFront);
+ al_clear_to_color(blankColour);
+
+ GamePanel testPanel(testBitmapBack, testBitmapFront, 0, 0, 500, 500);
+ Maze testMaze;
+
+ vector<pair<int,int> > wallsFull;
+
+ for (int x=0; x<20; ++x)
+ {
+ for (int y=0; y<20; ++y)
+ {
+ wallsFull.push_back(make_pair(x,y));
+ }
+ }
+ testMaze.generateMaze(wallsFull,20,20);
+
+ list<PlayerCar> players;
+ list<EnemyCar> enemies;
+ list<Checkpoint> checkpoints;
+ list<Rock> rocks;
+ list<Smokescreen> smokescreens;
+ list<DestroyedObjectPopup> popups;
+
+ testPanel.flip();
+
+ testPanel.draw(testMaze, players, enemies, checkpoints, rocks, smokescreens, popups);
+
+ BitmapStore bitmapStore(50);
+ ALLEGRO_BITMAP* wall = bitmapStore.getBitmap(BitmapStore::MAZE_WALL);
+ ALLEGRO_COLOR mazeColour = al_get_pixel(wall, 25, 25);
+
+ ALLEGRO_COLOR backSample = al_get_pixel(testBitmapBack, 250, 250);
+ ALLEGRO_COLOR frontSample = al_get_pixel(testBitmapFront, 250, 250);
+
+ EXPECT_FLOAT_EQ(mazeColour.r, frontSample.r);
+ EXPECT_FLOAT_EQ(mazeColour.g, frontSample.g);
+ EXPECT_FLOAT_EQ(mazeColour.b, frontSample.b);
+ EXPECT_FLOAT_EQ(mazeColour.a, frontSample.a);
+
+ EXPECT_FLOAT_EQ(blankColour.r, backSample.r);
+ EXPECT_FLOAT_EQ(blankColour.g, backSample.g);
+ EXPECT_FLOAT_EQ(blankColour.b, backSample.b);
+ EXPECT_FLOAT_EQ(blankColour.a, backSample.a);
+
+ al_destroy_bitmap(testBitmapBack);
+ al_destroy_bitmap(testBitmapFront);
+}