As the title clearly states, difficult terrain is going to be hard. It will involve a replacement of the ‘get distance’ code that takes into account that strict distances are not the same as movement distances – the idea that four inches of distance may cost five, six or more inches of movement (which is what counts for running, charging, and moving in general).
Because of this difficulty, my current inability to devise a solution, and my need to get moving on other things immediately (time on this project is limited, and I would rather move on than get stuck in design hell that maybe I should have sorted out sooner) I will be abandoning the Movement phase for the infinitely more interesting Shooting phase. It actually looks slightly easier, but I’m sure that’ll be remedied when I get to the rules for weapons and skills.
Unlike movement, which started to bog down the Model class, I’m going to try and keep all shooting related code in the shooting manager. Again, having an inkling of what the skills and weapons rules are going to do later, I believe this will make things a lot easier.
The first thing written down is defining who can shoot – Any model can shoot as long as it’s armed with a missile weapon, is not in hand-to-hand combat, can see a target, didn’t run or charge and is not hiding. This seems like a sensible thing to write a checking method for, and a little raft of tests. Time for a new test class, and also (finally) a test helper.
class TestHelper
{
public static IGameManager GetMockGameManager(IModel testModel, IObjectManager objectManager, int numberOfEnemies, int numberOfAllies)
{
Mock<IGameManager> mockGameManager = new Mock<IGameManager>();
List<IModel> modelList = new List<IModel>();
modelList.Add(testModel);
for (int i = 0; i < numberOfAllies; i++)
{
modelList.Add(new Model(testModel.Player, mockGameManager.Object, objectManager));
}
for (int i = 0; i < numberOfEnemies; i++)
{
modelList.Add(new Model(testModel.Player + 1, mockGameManager.Object, objectManager));
}
mockGameManager.Setup(item => item.Models).Returns(modelList);
return mockGameManager.Object;
}
}
[TestClass]
public class ShootingManagerTests
{
[TestMethod]
public void CanShoot_ReturnsTrue_IfEverythingCorrect()
{
Model model = new Model(1, null, null);
model.Weapons.Add(new MissileWeapon());
TestHelper.GetMockGameManager(model, null, 1, 0);
model.Location = new LocationPoint(0, 1, 0);
model.Direction = 180;
model.IsCharging = false;
model.IsHiding = false;
model.IsRunning = false;
ShootingManager shootingTest = new ShootingManager();
Assert.IsTrue(shootingTest.CanShoot(model));
}
}
Right, a lot of ‘generate class’ and ‘generate stub’ later (the ctrl+. shortcut is my friend) and the library compiles again. I intend for all missile weapons (that need special functionality not covered by the basic rules) to inherit from MissileWeapon, which ought to cover the majority of things. Weapons is a list of IWeapon (implemented by MissileWeapon), Direction is just an integer describing the angle the model faces (assuming a top-down view), and ShootingManager is quite an empty class at the moment. Following TDD pretty strictly, to pass this test it becomes:
public class ShootingManager
{
public bool CanShoot(Model model)
{
bool canShoot = true;
return canShoot;
}
}
But of course, that’s not quite right. Here’s a few more tests.
[TestMethod]
public void CanShoot_ReturnsFalse_IfRunning()
{
Model model = new Model(1, null, null);
model.Weapons.Add(new MissileWeapon());
TestHelper.GetMockGameManager(model, null, 1, 0);
model.Location = new LocationPoint(0, 1, 0);
model.Direction = 180;
model.IsCharging = false;
model.IsHiding = false;
model.IsRunning = true;
ShootingManager shootingTest = new ShootingManager();
Assert.IsFalse(shootingTest.CanShoot(model));
}
[TestMethod]
public void CanShoot_ReturnsFalse_IfCharging()
{
Model model = new Model(1, null, null);
model.Weapons.Add(new MissileWeapon());
TestHelper.GetMockGameManager(model, null, 1, 0);
model.Location = new LocationPoint(0, 1, 0);
model.Direction = 180;
model.IsCharging = true;
model.IsHiding = false;
model.IsRunning = false;
ShootingManager shootingTest = new ShootingManager();
Assert.IsFalse(shootingTest.CanShoot(model));
}
[TestMethod]
public void CanShoot_ReturnsFalse_IfHiding()
{
Model model = new Model(1, null, null);
model.Weapons.Add(new MissileWeapon());
TestHelper.GetMockGameManager(model, null, 1, 0);
model.Location = new LocationPoint(0, 1, 0);
model.Direction = 180;
model.IsCharging = false;
model.IsHiding = true;
model.IsRunning = false;
ShootingManager shootingTest = new ShootingManager();
Assert.IsFalse(shootingTest.CanShoot(model));
}
And what do all those lines of code need to make them pass?
public bool CanShoot(Model model)
{
bool canShoot = true;
if (model.IsRunning == true || model.IsHiding == true || model.IsCharging == true)
{
canShoot = false;
}
return canShoot;
}
It’s almost insulting. The next one should be a bit more of a challenge:
[TestMethod]
public void CanShoot_ReturnsFalse_IfNoMissileWeapon()
{
Model model = new Model(1, null, null);
TestHelper.GetMockGameManager(model, null, 1, 0);
model.Location = new LocationPoint(0, 1, 0);
model.Direction = 180;
model.IsCharging = false;
model.IsHiding = false;
model.IsRunning = false;
ShootingManager shootingTest = new ShootingManager();
Assert.IsFalse(shootingTest.CanShoot(model));
}
[TestMethod]
public void CanShoot_ReturnsFalse_IfOnlyMeleeWeapons()
{
Model model = new Model(1, null, null);
model.Weapons.Add(new MeleeWeapon());
TestHelper.GetMockGameManager(model, null, 1, 0);
model.Location = new LocationPoint(0, 1, 0);
model.Direction = 180;
model.IsCharging = false;
model.IsHiding = false;
model.IsRunning = false;
ShootingManager shootingTest = new ShootingManager();
Assert.IsFalse(shootingTest.CanShoot(model));
}
And these are solved by:
public bool CanShoot(Model model)
{
bool canShoot = true;
if (model.IsRunning == true || model.IsHiding == true || model.IsCharging == true)
{
canShoot = false;
}
if (model.Weapons.Where(item => (item as MissileWeapon) != null).Count() == 0)
{
canShoot = false;
}
return canShoot;
}
Linq makes things a little bit too easy these days… The next test checks if the model can see an opponent:
[TestMethod]
public void CanShoot_ReturnsFalse_IfNoEnemyInSight()
{
Model model = new Model(1, null, null);
model.Weapons.Add(new MissileWeapon());
TestHelper.GetMockGameManager(model, null, 1, 0);
model.Location = new LocationPoint(0, 1, 0);
model.Direction = 0;
model.IsCharging = false;
model.IsHiding = false;
model.IsRunning = false;
ShootingManager shootingTest = new ShootingManager();
Assert.IsFalse(shootingTest.CanShoot(model));
}
As expected, it fails. The only difference to the original ‘everything is fine’ test case is the direction that the model is facing – a model can only see in a 90 degree arc to it’s front, so by turning it completely around it should be unable to see the enemy properly. The calculations on the abstract ObjectManager should reflect this, so an extra method in ObjectManager that is exposed (slightly) through Model will help out.
First off – a lot of tests break. This is because the models have been set up with null object managers, which obviously throw exceptions when the call to check them for models in sight occurs. In refactoring to get the first test (pass if all clear) passing, references and parameters were shifted from Model to IModel, things were added to IModel,and in general a bit of housekeeping took place. Here is the new ‘all clear’ test:
[TestMethod]
public void CanShoot_ReturnsTrue_IfEverythingCorrect()
{
IModel model = new Model(0, null, null);
IGameManager gameManager = TestHelper.GetMockGameManager(model, null, 1, 0);
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
mockObjectManager.Setup(item => item.GetModelsInSight(It.IsAny<IModel>(), gameManager.Models, 90)).Returns(gameManager.Models);
model = new Model(1, gameManager, mockObjectManager.Object);
model.Weapons.Add(new MissileWeapon());
model.Location = new LocationPoint(0, 1, 0);
model.Direction = 180;
model.IsCharging = false;
model.IsHiding = false;
model.IsRunning = false;
ShootingManager shootingTest = new ShootingManager();
Assert.IsTrue(shootingTest.CanShoot(model));
}
With similar ‘setup’ code to the subsequent tests to get them all passing. The improved ‘new’ test:
[TestMethod]
public void CanShoot_ReturnsFalse_IfNoEnemyInSight()
{
IModel model = new Model(0, null, null);
IGameManager gameManager = TestHelper.GetMockGameManager(model, null, 1, 0);
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
mockObjectManager.Setup(item => item.GetModelsInSight(It.IsAny<IModel>(), gameManager.Models, 90)).Returns(new List<IModel>());
model = new Model(1, gameManager, mockObjectManager.Object);
model.Weapons.Add(new MissileWeapon());
model.Location = new LocationPoint(0, 1, 0);
model.Direction = 0;
model.IsCharging = false;
model.IsHiding = false;
model.IsRunning = false;
ShootingManager shootingTest = new ShootingManager();
Assert.IsFalse(shootingTest.CanShoot(model));
}
And finally, the code to pass it:
public bool CanShoot(IModel model)
{
bool canShoot = true;
if (model.IsRunning == true || model.IsHiding == true || model.IsCharging == true)
{
canShoot = false;
}
if (model.Weapons.Where(item => (item as MissileWeapon) != null).Count() == 0)
{
canShoot = false;
}
if ((from target in model.GetModelsInSight(90)
where target.Player != model.Player
select target).Count() == 0)
{
canShoot = false;
}
return canShoot;
}
With one small addition to the Model class:
public List<IModel> GetModelsInSight(int angleOfSight)
{
List<IModel> modelsInSight = new List<IModel>();
modelsInSight = _objectManager.GetModelsInSight(this, _gameManager.Models, angleOfSight);
return modelsInSight;
}
Finally, one more test and the ‘who can shoot’ question is completely answered. Models cannot shoot if they’re already in hand-to-hand combat. A simple boolean property (the exact circumstances of being able to set it will be complicated, but that’s a problem for later) on the Model and IModel, adding it as an explicit ‘false’ parameter in the other CanShoot tests, and it’s own test case:
[TestMethod]
public void CanShoot_ReturnsFalse_IfModelInHandToHandCombat()
{
IModel model = new Model(0, null, null);
IGameManager gameManager = TestHelper.GetMockGameManager(model, null, 1, 0);
Mock<IObjectManager> mockObjectManager = new Mock<IObjectManager>();
mockObjectManager.Setup(item => item.GetModelsInSight(It.IsAny<IModel>(), gameManager.Models, 90)).Returns(gameManager.Models);
model = new Model(1, gameManager, mockObjectManager.Object);
model.Weapons.Add(new MissileWeapon());
model.Location = new LocationPoint(0, 1, 0);
model.Direction = 180;
model.IsCharging = false;
model.IsHiding = false;
model.IsRunning = false;
model.IsInHandToHandCombat = true;
ShootingManager shootingTest = new ShootingManager();
Assert.IsFalse(shootingTest.CanShoot(model));
}
And the complete CanShoot code to pass it:
public bool CanShoot(IModel model)
{
bool canShoot = true;
if (model.IsRunning == true || model.IsHiding == true || model.IsCharging == true || model.IsInHandToHandCombat == true)
{
canShoot = false;
}
if (model.Weapons.Where(item => (item as MissileWeapon) != null).Count() == 0)
{
canShoot = false;
}
if ((from target in model.GetModelsInSight(90)
where target.Player != model.Player
select target).Count() == 0)
{
canShoot = false;
}
return canShoot;
}
And it leaves me at a good point to finish off for this section. The next section will be the Closest Target rules, which I’ve already given some thought to – but my word count here is already over 1500, so it’s probably best to clock off!