The Endgame

The game will end if any of the following conditions ever become true

  1. All the monsters are dead
  2. The player is dead

An actor is dead when it’s life has been reduced to zero or less. In order to implement the above I had to give some thought to the game loop and how the player and monsters would fit into the game.  I came up with the following unit test to describe the behaviour I was looking for.

   10         [Test]

   11         public void TestGameEndsWhenAllMonstersAreDead()

   12         {

   13             // Create a new game instance

   14             var game = new Game();

   15             // Initialize the game with a player actor and a few dead monsters

   16             game.Initialize(new Actor {HitPoints = 10},

   17                             new List<IActor>

   18                                 {

   19                                     new Actor {HitPoints = 0},

   20                                     new Actor {HitPoints = 0}

   21                                 });

   22 

   23             // Process a game turn

   24             game.ProcessTurn();

   25             // Assert that the game has ended

   26             Assert.IsFalse(game.IsActive);

   27         }

In the above test I create a new instance of my game and initialize it with a live player and some dead monsters. I foresee this initialization step to happen when we start our game, or load an existing game or maybe when changing levels. For now I’m keeping it simple.

I then foresee the game loop to keep processing turns while the game is active. Of course this will include things like rendering and reacting to player input, but I’m not worried about this right now.

The ProcessTurn method is responsible for checking if the monsters are all dead and then setting the IsActive flag to false.

The following test verifies that the game ends when the player dies.

   29         [Test]

   30         public void TestGameEndsWhenThePlayerIsDead()

   31         {

   32             // Create a new game instance

   33             var game = new Game();

   34             // Initialize the game with a dead player actor and a live monster

   35             game.Initialize(new Actor {HitPoints = 0},

   36                             new List<IActor>

   37                                 {

   38                                     new Actor {HitPoints = 5}

   39                                 });

   40             // Process a game turn

   41             game.ProcessTurn();

   42             // Assert that the game has ended

   43             Assert.IsFalse(game.IsActive);

   44         }

This test is exactly the same as before except that the player is dead and the monster alive. No real rocket science. I added the following test to make sure that the game is in fact active when both the player and at least one monster is alive.

   46         [Test]

   47         public void TestGameIsActiveWhenThePlayerIsAliveAndAMonsterIsAlive()

   48         {

   49             // Create a new game instance

   50             var game = new Game();

   51             // Initialize the game with a live player actor, a live monster,

   52             // and a dead monster

   53             game.Initialize(new Actor {HitPoints = 10},

   54                             new List<IActor>

   55                                 {

   56                                     new Actor {HitPoints = 5},

   57                                     new Actor {HitPoints = 0}

   58                                 });

   59             // Process a game turn

   60             game.ProcessTurn();

   61             // Assert that the game has ended

   62             Assert.IsTrue(game.IsActive);

   63         }

This test initializes the game with a live player and monster and a dead monster (just to mix things up a bit).

I feel the above tests have given me enough functionality to satisfy my end game requirements. I am going to have to do some work on the ProcessTurn method to implement actors taking turns and checking that dead actors don’t get a chance to act. So far I’m happy with the design (I haven’t had to make any major changes to implement a feature yet). I do have a lot of thoughts on new features I’d like to implement, but I’m trying to be really disciplined on keeping to my original scope.

Advertisements

The speed system

My speed system will have the following characteristics:

  • All actors (who are able) should have at least one turn per game turn.
  • Faster actors should take their turns before slower actors.
  • If Bob is twice as fast as Fred then Bob should have two turns per Fred’s one turn.
  • The game turn ends when all actors have had at least one turn.

To implement the above features I came up with the following unit tests. For all the unit tests I create a GameTurn object that is initialized with two identical actors. Each of these actors have a speed of 1 be default.

The GameTurn class is responsible for implementing the speed system features. In order for me to test the functionality I let the ProcessTurn method return a list of GameEvents completed during the turn. The GameEvent class holds information such as the actor, the action and the time interval at which a turn took place.

   50         // Every actor should have at least one turn per game turn

   51         [Test]

   52         public void TestEachActorHadLeastOneTurn()

   53         {

   54             var actions = gameTurn.ProcessTurn();

   55 

   56             Assert.IsTrue(actions.Any(x => x.Actor == actor1));

   57             Assert.IsTrue(actions.Any(x => x.Actor == actor2));

   58         }

In the above test I process the game turn and check that each actor has performed at least one action. The implementation for the above test is fairly trivial. We just loop through each of the actors and add their next action to a list and return it when we’re done.

   60         // Actors take their turns in sequence based on their speed

   61         [Test]

   62         public void TestEachActorTakesHisTurnInOrder()

   63         {

   64             actor1.Speed = 2;

   65             actor2.Speed = 3;

   66 

   67             var actions = gameTurn.ProcessTurn();

   68 

   69             Assert.IsTrue(actions[0].Actor == actor1);

   70             Assert.IsTrue(actions[1].Actor == actor2);

   71         }

In the above test I make sure that each actor is processed in the correct sequence. The sequence is determined by the actor speed. Lower speed values are processed before higher speed values. The implementation for the above test makes use of a priority queue that is keyed to the actor speed. The ProcessTurn method now asks the priority queue for the next actor until all the actors have had their turn.

   73         // The slowest actor should only act once per game turn

   74         [Test]

   75         public void TestSlowestActorHadOneTurn()

   76         {

   77             actor2.Speed = 2;

   78 

   79             var actions = gameTurn.ProcessTurn();

   80 

   81             Assert.AreEqual(1, actions.Count(x => x.Actor == actor2));

   82         }

In the above test we make sure that the slowest actor has only one turn. The game turn must end when the slowest actor has completed its turn. To implement this feature I evaluate the list of actions that have already been completed before processing the next actor. I stop processing actors when all the actors have had their turn.

   84         // The actor with half the speed should act twice per turn

   85         [Test]

   86         public void TestHalfSpeedActorHadTwoTurns()

   87         {

   88             actor2.Speed = 2;

   89 

   90             var actions = gameTurn.ProcessTurn();

   91 

   92             Assert.AreEqual(2, actions.Count(x => x.Actor == actor1));

   93             Assert.AreEqual(1, actions.Count(x => x.Actor == actor2));

   94         }

In the above test we make sure that actor1 had two turns while actor2 only had one turn. My first pass of the test failed with unexpected results. Each actor only completed one turn even though actor1 with twice the speed of actor2 was in the priority queue. The reason for the failure was that check that everyone had completed their turn was true even though actor1 had not completed it’s second turn. To solve this I added an additional check that the game turn would only end if the last actor had completed it’s turn and all other actors to act at the same time had completed theirs.

The final ProcessTurn method is implemented as follows.

   19         public IList<GameEvent> ProcessTurn()

   20         {

   21             var completedActions = new List<GameEvent>();

   22 

   23             InitializeCommandQueue();

   24 

   25             var lastTimeSlice = -1;

   26 

   27             // We process the command queue while there are actors to process

   28             while (commandQueue.Count > 0)

   29             {

   30                 // Get the next actor to process

   31                 var nextTimeInterval = commandQueue.Peek();

   32 

   33                 // stop processing if everyone has had a turn and we’re on a new time slice

   34                 if ((AllActorsHaveActed(completedActions))

   35                     && (lastTimeSlice != nextTimeInterval.Key))

   36                     break;

   37 

   38                 // Update the last time slice to the current time slice

   39                 lastTimeSlice = nextTimeInterval.Key;

   40 

   41                 // Remove dead actors from the queue

   42                 if (!nextTimeInterval.Value.IsAlive)

   43                 {

   44                     commandQueue.Dequeue();

   45                     continue;

   46                 }

   47 

   48                 // Get the next action from the actor

   49                 ICommand nextAction = nextTimeInterval.Value.GetNextAction();

   50                 if ((nextAction != null) && (nextAction.Execute()))

   51                 {

   52                     // Remove the actor from the queue if the action was successful

   53                     commandQueue.Dequeue();

   54                     // Add the successful command to the list of completed actions

   55                     completedActions.Add(new GameEvent

   56                         {   Actor = nextTimeInterval.Value,

   57                             Command = nextAction,

   58                             TimeSlice = nextTimeInterval.Key

   59                         });

   60                     // Add the actor to the queue incrementing the priority with the speed

   61                     commandQueue.Enqueue(

   62                         nextTimeInterval.Key + nextTimeInterval.Value.Speed,

   63                         nextTimeInterval.Value);

   64                 }

   65                 else

   66                     // We stop processing actions for this game loop cycle as

   67                     // the last action was unsuccessful

   68                     break;

   69             }

   70 

   71             return completedActions;

   72         }

A simple game

I have realised that the only way for me to actually make any real progress is to implement a very simple game. The aim of the game will be to kill all your opponents on a single map (probably a square room to start with).

I hope that by implementing this very limited scope I will be able to complete the following features:

  • Game loop
  • Speed system (variable speed actors)
  • Death (an actor dies, the effect on the game loop etc.)
  • Health
  • Damage
  • The end game (i.e. you have died or all your opponents are dead)

Attacking will be simplified by allowing actors to always hit an opponent when they attack. I’m thinking of having actors with different health and damage values. For example there would be a tank type with low damage but high health and a DPS type with high damage and low health. These two types would require different strategies to overcome.

Nothing too complicated. Lets hope I can implement it without too much interference from real life.