/** @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 #include #include using namespace std; #include #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 > walls; testMaze.generateMaze(walls,5,5); PlayerCar player(2,2,Maze::UP); list 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 > walls; testMaze.generateMaze(walls,5,5); PlayerCar player(2,2,Maze::DOWN); list 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 > walls; testMaze.generateMaze(walls,5,5); PlayerCar player(2,2,Maze::LEFT); list 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 > walls; testMaze.generateMaze(walls,5,5); PlayerCar player(2,2,Maze::RIGHT); list 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 > walls; walls.push_back(make_pair(3,3)); testMaze.generateMaze(walls,5,5); PlayerCar player(3,4,Maze::UP); list 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 > walls; walls.push_back(make_pair(3,3)); testMaze.generateMaze(walls,5,5); PlayerCar player(3,2,Maze::DOWN); list 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 > walls; walls.push_back(make_pair(3,3)); testMaze.generateMaze(walls,5,5); PlayerCar player(4,3,Maze::LEFT); list 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 > walls; walls.push_back(make_pair(3,3)); testMaze.generateMaze(walls,5,5); PlayerCar player(2,3,Maze::RIGHT); list 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 > walls; testMaze.generateMaze(walls,5,5); PlayerCar player(3,0,Maze::UP); list 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 > walls; testMaze.generateMaze(walls,5,5); PlayerCar player(3,5,Maze::DOWN); list 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 > walls; testMaze.generateMaze(walls,5,5); PlayerCar player(0,3,Maze::LEFT); list 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 > walls; testMaze.generateMaze(walls,5,5); PlayerCar player(5,3,Maze::RIGHT); list 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 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 players; list enemies; list checkpoints; list rocks; list 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::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::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 players; list enemies; list checkpoints; list rocks; list 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::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::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 players; list enemies; list checkpoints; list rocks; list 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::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::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 players; list enemies; list checkpoints; list rocks; list 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::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 > 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 > 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 > 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 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 > walls; testMaze.generateMaze(walls,5,5); PlayerCar player(5,3,Maze::RIGHT); list 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); }