(#100DaysOfCode) Day Three – You Get All Sorts of Characters Here

To prove I’m an (almost) human being I will occasionally let you know about things I like. Things that interest me. This may mean that from time to time posting links to artists or products or websites. I’d just like you to know that I am not endorsing any of these, nor am I receiving any advertising revenue for them. They are simply things that interest me and may or may not interest you. Just thought I’d get that out into the open.

Despite being fiscally challenged, we do own a dishwasher. I’ve been told by my wife that it’s not particularly good at washing dishes, but it is relatively economical and so long as it does what it’s told she won’t be swapping it for a new model anytime soon.

So whilst I wash the dishes for my family I often listen to audio books. I’m currently working my way through the Laundry Files series of books by Charles Stross. At other times I listen to music. More often than not this is loud music with screaming guitars often contrasting with soft (or screamy) feminine vocals. I thought I’d share a list of some of my favourites. This isn’t exactly in order of preference because I’m not sure I have an actual preference:-

Some golden oldies:-

I’m also listening to a lot to Radio Caprice, an internet radio station that plays metal songs with female vocals. The songs played here are quite diverse (within the genre) which brings me to an interesting point. When adding characters to a game, diversity is the key. I want the player to be able to interact with a wide range of characters, all with their own individual backgrounds, quirks, goals and personalities. That’s the ultimate goal here.

Having said that we do need to begin somewhere and its best to start small. So we will begin by dealing with (arguably) the most important character of all – the Player Character.

Wanna play a game?

My aim is that after selecting a new game, the player will select a pre-defined character from a list presented to them. They will be able to change the name, gender and sexual orientation of their selected character but nothing else. Perhaps in the future, I’ll add a custom character creation module but for now, I think this is a good ‘just enough’ approach.

In the GameEngine project I added a new Character class. This will potentially hold the details of ALL characters in the game, both player and non-player characters:-

using Cybermancer.CelestialAberration.GameEngine.Enumerations;
using System.Collections.Generic;

namespace Cybermancer.CelestialAberration.GameEngine.GameObjects
{
    public class Character
    {
        public string CharacterId { get; set; }

        public List<string> FirstNames { get; set; }

        public string LastName { get; set; }

        public string FamiliarName { get; set; }

        public string FormalTitle { get; set; }

        public Gender Gender { get; set; }

        public SexualOrientation SexualOrientation { get; set; }

        public string Nationality { get; set; }

        public Position Position { get; set; }

        public string Biography { get; set; }
    }
}

This class relies on some enumerations (Gender, Position, SexualOrientation) That I created under an Enumerations directory:-

Gender.cs:-

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Cybermancer.CelestialAberration.GameEngine.Enumerations
{
    [JsonConverter(typeof(StringEnumConverter))]
    public enum Gender
    {
        Neutral,
        Male,
        Female
    }
}
Position.cs:-

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Cybermancer.CelestialAberration.GameEngine.Enumerations
{
    [JsonConverter(typeof(StringEnumConverter))]
    public enum Position
    {
        Captain,
        Engineer,
        CommunicationsOfficer,
        FirstOfficer,
        GunneryOfficer,
        MedicalOfficer,
        Pilot,
        Scientist,
        SecurityOfficer
    }
}

SexualOrientation.cs:-

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Cybermancer.CelestialAberration.GameEngine.Enumerations
{
    [JsonConverter(typeof(StringEnumConverter))]
    public enum SexualOrientation
    {
        Asexual,
        Bisexual,
        Homosexual,
        Heterosexual
    }
}

The attribute decorator ([JsonConverter(typeof(StringEnumConverter))]) ensures that if the object gets serialized (e.g. if game data is saved back to a data store), then the JSON generated will contain the actual string values of the enumerations and NOT their integer values. This not only helps to make the JSON more human-readable, but it allows us to add additional enumeration values in the future without having to explicitly set their integer value equivalents in our source code.

So how do we know which characters are player templates? Well in this game the player is the captain of an experimental star ship so all characters with the Position property of Position.Captain will be a player template. All others will be NPCs (this may change in a future implementation as the scope of work evolves).

A collection of characters is held in a new CharacterLibrary Class. This is similar to the GameInfo class created on Day One and also has an overloaded ToString() method. Since this method is a common method that will be used by many classes I have decided to create a base SerializableGameObject class that holds just this method, and changed GameInfo to derive from this class, as well as having our new CharacterLibrary class derive from it also:-

SerializableGameObject.cs:-

using Newtonsoft.Json;

namespace Cybermancer.CelestialAberration.GameEngine.GameObjects
{
    public class SerializableGameObject
    {
        public override string ToString()
        {
            return JsonConvert.SerializeObject(this);
        }
    }
}

CharacterLibrary.cs:-

using System.Collections.Generic;
using System.Linq;
using Cybermancer.CelestialAberration.GameEngine.Enumerations;

namespace Cybermancer.CelestialAberration.GameEngine.GameObjects
{
    public class CharacterLibrary : SerializableGameObject
    {
        public List<Character> Characters { get; set; }

        public List<Character> GetCharacters(Position position)
        {
            return Characters.Where(character => character.Position == position).ToList();
        }

        public Character GetCharacter(string characterId)
        {
            return Characters.Where(character => character.CharacterId == characterId).FirstOrDefault();
        }
    }
}

Note that we have two methods in our CharacterLibrary class. The GetCharacters(Position position) method will return a list of characters matching a given position so when trying to retrieve all player templates we would call characterLibraryInstance.GetCharacters(Position.Captain). The GetCharacter(string characterId) method will retrieve a single character object so when trying to select the first player character from the list we would call characterLibraryInstance.GetCharacter(“PLAYER_1”).

Our library of game characters will be held as another JSON file as an embedded resource in our Resources directory (not forgetting to set the build action to ‘Embedded Resource’ like I did).

{
  "characters": [
    {
      "CharacterId": "PLAYER_1",
      "FirstNames": [
        "Richard",
        "Alexander"
      ],
      "LastName": "Stone",
      "FamiliarName": "Richard",
      "FormalTitle": "Mr",
      "Gender": "Male",
      "SexualOrientation": "Heterosexual",
      "Nationality": "English",
      "Position": "Captain",
      "Biography": "TBA"
    },
    {
      "CharacterId": "PLAYER_2",
      "FirstNames": [
        "Nozomi"
      ],
      "LastName": "Tanaka",
      "FamiliarName": "Nozomi",
      "FormalTitle": "Miss",
      "Gender": "Female",
      "SexualOrientation": "Bisexual",
      "Nationality": "Japanese",
      "Position" : "Captain",
      "Biography" : "TBA"
    },
    {
      "CharacterId": "PLAYER_3",
      "FirstNames": [
        "Zaheed"
      ],
      "LastName": "Bagwala",
      "FamiliarName": "Zah",
      "FormalTitle": "Mr",
      "Gender": "Male",
      "SexualOrientation": "Homosexual",
      "Nationality": "Indian",
      "Position" : "Captain",
      "Biography" : "TBA"
    },
    {
      "CharacterId": "PLAYER_4",
      "FirstNames": [
        "E.V.E"
      ],
      "LastName": "",
      "FamiliarName": "Eve",
      "FormalTitle": "",
      "Gender": "Neutral",
      "SexualOrientation": "Asexual",
      "Nationality": "American",
      "Position" : "Captain",
      "Biography" : "TBA"
    }
  ]
}

I was going to give each character a detailed biography but time prevented me from doing so. I will do so on Day Four as this is what, in reality, will make each character ‘unique’.

We now have to update the IGameResourceRepository interface and the GameResourceAssemlyRepository class to add a method to get this resource from the assembly and pass it to the application.

IGameResourceRepository.cs:-

using Cybermancer.CelestialAberration.GameEngine.GameObjects;

namespace Cybermancer.CelestialAberration.GameEngine.Repositories
{
    public interface IGameResourceRepository
    {
        GameIntro GetGameIntro(string resourceId);

        CharacterLibrary GetCharacterLibrary(string resourceId);
    }
}
GameResourceAssemblyRepository.cs:-

using Cybermancer.CelestialAberration.GameEngine.GameObjects;
using Newtonsoft.Json;
using System.IO;
using System.Reflection;

namespace Cybermancer.CelestialAberration.GameEngine.Repositories
{
    public class GameResourceAssemblyRepository : IGameResourceRepository
    {
        public GameIntro GetGameIntro(string resourceId)
        {
            return JsonConvert.DeserializeObject<GameIntro>(GetResourceFileAsString(resourceId, "GameIntro.json"));
        }

        public CharacterLibrary GetCharacterLibrary(string resourceId)
        {
            return JsonConvert.DeserializeObject<CharacterLibrary>(GetResourceFileAsString(resourceId, "CharacterLibrary.json"));
        }

        private string GetResourceFileAsString(string resourceId, string resourceName)
        {
            var assembly = Assembly.GetExecutingAssembly();
            var resourcePath = $"Cybermancer.CelestialAberration.GameEngine.GameResources.{resourceId}.{resourceName}";

            using (Stream stream = assembly.GetManifestResourceStream(resourcePath))
            using (StreamReader reader = new StreamReader(stream))
            {
                return reader.ReadToEnd();
            }
        }
    }
}

Finally, we will scope out the object that will ultimately hold data about an actual game in progress:-

namespace Cybermancer.CelestialAberration.GameEngine.GameObjects
{
    public class GameData : SerializableGameObject
    {
        public string Id { get; set; }
        public string Language { get; set; }
        public Character PlayerCharacter { get; set; }
    }
}

The Id property will uniquely identify this particular game. The Language property is intended to tie the game to a particular resource set for future multi-lingual implementations. Current this will just be set to “DEFAULT” and will be the resource id passed to the retrieval methods in IGameResourceRepository implementations.

We won’t be doing much with this class yet but on Day Four we will be making use of this during ‘new game’ game creation and player character selection.

A Test of One’s Character

We will now extend our repository tests to include tests for retrieving the CharacterLibrary from the assembly. Note that I am writing unit tests AFTER writing code. This is not strictly what should be done in Test Driven Development so in future blogs I may actually attempt to do this properly (i.e. write the tests THEN write the code to make them pass).

GameResourceRepositoryTests.cs:-

using System.Linq;
using Cybermancer.CelestialAberration.GameEngine.Enumerations;
using Cybermancer.CelestialAberration.GameEngine.Repositories;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Cybermancer.CelestialAberration.GameEngineTests.RepositoryTests
{
    [TestClass]
    public class GameResourceRepositoryTests
    {
        [TestMethod]
        public void EnsureRepositoryRetrievesGameInfoCorrectly()
        {
            IGameResourceRepository repository = new GameResourceAssemblyRepository();

            var gameIntro = repository.GetGameIntro("Default");

            var actualIntroText = gameIntro.IntroTexts.FirstOrDefault();

            Assert.IsNotNull(actualIntroText);

            Assert.AreEqual("INTRO_0", actualIntroText.GameTextId);
            Assert.AreEqual(
                "I have seen the dark universe yawning[br]Where the black planets roll without aim,[br]Where they roll in their horror unheeded,[br]Without knowledge, or lusture, or name.[br][br]- H.P. Lovecraft",
                actualIntroText.Content);
            Assert.AreEqual("neon", actualIntroText.Style);
            Assert.AreEqual(15, actualIntroText.SecondsToDisplay);
        }

        [TestMethod]
        public void EnsureRepositoryRetrievesCharacterLibraryCorrectly()
        {
            IGameResourceRepository repository = new GameResourceAssemblyRepository();

            var characterLibrary = repository.GetCharacterLibrary("Default");

            var actualCharacter = characterLibrary.Characters.FirstOrDefault();

            Assert.IsNotNull(actualCharacter);

            Assert.AreEqual("PLAYER_1", actualCharacter.CharacterId);
            Assert.AreEqual("Richard",actualCharacter.FirstNames.FirstOrDefault());
            Assert.AreEqual("Alexander", actualCharacter.FirstNames.LastOrDefault());
            Assert.AreEqual("Stone", actualCharacter.LastName);
            Assert.AreEqual("Richard", actualCharacter.FamiliarName);
            Assert.AreEqual("Mr", actualCharacter.FormalTitle);
            Assert.AreEqual(Gender.Male, actualCharacter.Gender);
            Assert.AreEqual(SexualOrientation.Heterosexual, actualCharacter.SexualOrientation);
            Assert.AreEqual("English", actualCharacter.Nationality);
            Assert.AreEqual(Position.Captain, actualCharacter.Position);
            Assert.AreEqual("TBA", actualCharacter.Biography);
        }
    }
}

We can also test the methods of the character library class to retrieve a specific character or a group of characters. There is a new test class called CharacterLibraryTests under a GameObjectTests directory.

using System.Collections.Generic;
using System.Linq;
using Cybermancer.CelestialAberration.GameEngine.Enumerations;
using Cybermancer.CelestialAberration.GameEngine.GameObjects;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Cybermancer.CelestialAberration.GameEngineTests.GameObjectTests
{
    [TestClass]
    public class CharacterLibraryTests
    {
        public CharacterLibrary CharacterLibrary { get; set; }

        [TestInitialize]
        public void TestSetup()
        {
            CharacterLibrary = new CharacterLibrary()
            {
                Characters = new List<Character>()
                {
                    new Character()
                    {
                        CharacterId = "PLAYER_1",
                        Position = Position.Captain
                    },
                    new Character()
                    {
                        CharacterId = "PLAYER_2",
                        Position = Position.Captain
                    },
                    new Character()
                    {
                        CharacterId = "FIRSTOFFICER_1",
                        Position = Position.FirstOfficer
                    },
                    new Character()
                    {
                        CharacterId = "ENGINEER_1",
                        Position = Position.Engineer
                    },
                    new Character()
                    {
                        CharacterId = "ENGINEER_2",
                        Position = Position.Engineer
                    },
                    new Character()
                    {
                        CharacterId = "ENGINEER_3",
                        Position = Position.Engineer
                    },
                }
            };
        }

        [TestMethod]
        public void EnsureThatCallingGetCharactersForCaptainPositionRetrievesCorrectCharacters()
        {
            var characters = CharacterLibrary.GetCharacters(Position.Captain);

            Assert.IsNotNull(characters);
            Assert.AreEqual(2, characters.Count);
            Assert.AreEqual("PLAYER_1", characters.First().CharacterId);
        }

        [TestMethod]
        public void EnsureThatCallingGetCharactersForFirstOfficerPositionRetrievesCorrectCharacters()
        {
            var characters = CharacterLibrary.GetCharacters(Position.FirstOfficer);

            Assert.IsNotNull(characters);
            Assert.AreEqual(1, characters.Count);
            Assert.AreEqual("FIRSTOFFICER_1", characters.First().CharacterId);
        }

        [TestMethod]
        public void EnsureThatCallingGetCharactersForEngineerPositionRetrievesCorrectCharacters()
        {
            var characters = CharacterLibrary.GetCharacters(Position.Engineer);

            Assert.IsNotNull(characters);
            Assert.AreEqual(3, characters.Count);
            Assert.AreEqual("ENGINEER_1", characters.First().CharacterId);
        }

        [TestMethod]
        public void EnsureThatCallingGetCharactersForNonExistantPositionRetrievesEmptyList()
        {
            var characters = CharacterLibrary.GetCharacters(Position.GunneryOfficer);

            Assert.IsNotNull(characters);
            Assert.AreEqual(0, characters.Count);
        }

        [TestMethod]
        public void EnsureThatCallingGetCharacterForExistingCharacterIdRetrievesCorectCharacter()
        {
            var character = CharacterLibrary.GetCharacter("ENGINEER_2");

            Assert.IsNotNull(character);
            Assert.AreEqual("ENGINEER_2", character.CharacterId);
        }

        [TestMethod]
        public void EnsureThatCallingGetCharacterForNonExistantCharacterIdRetrievesNull()
        {
            var character = CharacterLibrary.GetCharacter("GUNNER_1");

            Assert.IsNull(character);
        }
    }
}

Notice that in this test we are sharing test data between test methods, so we hold that data in a property and then populate it with a class marked [TestInitialize]. A method marked with this attribute will always run BEFORE the test method runs.

Here is a screenshot of our test output:-

That’s it for today. Tomorrow we will be back in the web app and adding functionality for the player to select a new game , a player template, and allow them to customise their name, gender and sexual orientation.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.