Lesson 4.15: The Wrap

This was another large chapter with many improvements to our game engine. Along the way we also learned some key concepts and design patterns that can be used in any .NET and C# project. Learning in the context of building a game is always more fun for me, so I hope you’re all enjoying the experience as well.

Over this chapter, we added the following features:

  • Attack and healing actions.
  • Consumable game items, like potions and granola bars.
  • Recipes for crafting items.
  • Keyboard navigation in a Blazor app/game.
  • Enhanced combat with attack initiative and hit/miss logic.
  • Loading all of our data from JSON files rather than hard-coded in source files.

As we implemented those new features, we learned about key concepts like:

  • The Command pattern and actions.
  • The Theory attribute for testing many similar test scenarios in Moq.
  • Blazor key processing events.
  • HTML accesskey support for keyboard accelerators.
  • The Observer pattern for building our DisplayMessageBroker.
  • Using Data Transfer Objects (DTO) pattern for JSON serialization.

Refactoring Model Classes

We made several significant refactoring changes when we were moving our data files to JSON, but in the last lesson we didn’t update model classes to also minimize their property surface area. We’re going to make the changes now, so that we exit this chapter with all of the clean-up work done.

1. We start by updating the ItemQuantity class to have a constructor and read-only properties:

using SimpleRPG.Game.Engine.Factories;

namespace SimpleRPG.Game.Engine.Models
{
    public class ItemQuantity
    {
        public ItemQuantity(int itemId, int quantity)
        {
            ItemId = itemId;
            Quantity = quantity;
        }
        public int ItemId { get; }

        public int Quantity { get; }

        public string QuantityItemDescription =>
            $"{ItemFactory.GetItemName(ItemId)} (x{Quantity})";
    }
}

2. Update the QuestFactory to use the ItemQuantity constructor:

using D20Tek.Common.Helpers;
using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class QuestFactory
    {
        private const string _resourceNamespace = "SimpleRPG.Game.Engine.Data.quests.json";
        private static readonly IList<QuestTemplate> _questTemplates = JsonSerializationHelper.DeserializeResourceStream<QuestTemplate>(_resourceNamespace);

        public static Quest GetQuestById(int id)
        {
            // first find the quest template by its id.
            var template = _questTemplates.First(p => p.Id == id);

            // then create an instance of quest from that template.
            var quest = new Quest(template.Id, template.Name, template.Description, 
                                  template.RewardGold, template.RewardXP);

            // next add each pre-requisite for the quest.
            foreach (var req in template.Requirements)
            {
                quest.ItemsToComplete.Add(new ItemQuantity(req.Id, req.Qty));
            }

            // finally add each reward item given from the quest.
            foreach(var item in template.RewardItems)
            {
                quest.RewardItems.Add(new ItemQuantity(item.Id, item.Qty));
            }

            return quest;
        }
    }
}

3. Update the Recipe class to also use the ItemQuantity constructor.

using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.Models
{
    public class Recipe
    {
        public Recipe(int id, string name)
        {
            Id = id;
            Name = name;
        }

        public int Id { get; }

        public string Name { get; } = string.Empty;

        public IList<ItemQuantity> Ingredients { get; } = new List<ItemQuantity>();

        public IList<ItemQuantity> OutputItems { get; } = new List<ItemQuantity>();

        public void AddIngredient(int itemId, int quantity)
        {
            if (!Ingredients.Any(x => x.ItemId == itemId))
            {
                Ingredients.Add(new ItemQuantity(itemId, quantity));
            }
        }

        public void AddOutputItem(int itemId, int quantity)
        {
            if (!OutputItems.Any(x => x.ItemId == itemId))
            {
                OutputItems.Add(new ItemQuantity(itemId, quantity));
            }
        }

        public DisplayMessage ToDisplayMessage()
        {
            var messageLines = new List<string>
            {
                "Ingredients:"
            };

            foreach (ItemQuantity q in Ingredients)
            {
                messageLines.Add(q.QuantityItemDescription);
            }

            messageLines.Add("Creates:");
            foreach (ItemQuantity itemQuantity in OutputItems)
            {
                messageLines.Add(itemQuantity.QuantityItemDescription);
            }

            return new DisplayMessage($"Recipe Added - {Name}", messageLines);
        }
    }
}

4. Refactor MonsterEncounter class to restrict its properties. We make the ChangeOfEncountering setter internal, so that it can be used within our game engine, but not externally.

namespace SimpleRPG.Game.Engine.Models
{
    public class MonsterEncounter
    {
        public MonsterEncounter(int monsterId, int chanceOfEncountering)
        {
            MonsterId = monsterId;
            ChanceOfEncountering = chanceOfEncountering;
        }

        public int MonsterId { get; }

        public int ChanceOfEncountering { get; internal set; }
    }
}

5. Remove the Quest default constructor and lock down all of its property setters:

using System;
using System.Collections.Generic;

namespace SimpleRPG.Game.Engine.Models
{
    public class Quest
    {
        public Quest(int id, string name, string description, int rewardsGold, int rewardsXP)
        {
            Id = id;
            Name = name;
            Description = description;
            RewardGold = rewardsGold;
            RewardExperiencePoints = rewardsXP;
        }

        public int Id { get; }

        public string Name { get; } = string.Empty;
        
        public string Description { get; } = string.Empty;

        public IList<ItemQuantity> ItemsToComplete { get; } = new List<ItemQuantity>();

        public int RewardExperiencePoints { get; }
        
        public int RewardGold { get; }
        
        public IList<ItemQuantity> RewardItems { get; } = new List<ItemQuantity>();

        public DisplayMessage ToDisplayMessage()
        {
            var messageLines = new List<string>
            {
                Description,
                "Items to complete the quest:"
            };

            foreach (ItemQuantity q in ItemsToComplete)
            {
                messageLines.Add(q.QuantityItemDescription);
            }

            messageLines.Add("Rewards for quest completion:");
            messageLines.Add($"{RewardExperiencePoints} experience points");
            messageLines.Add($"{RewardGold} gold");
            foreach (ItemQuantity itemQuantity in RewardItems)
            {
                messageLines.Add(itemQuantity.QuantityItemDescription);
            }

            return new DisplayMessage($"Quest Added - {Name}", messageLines);
        }
    }
}

With all of these code changes, our game engine will build successfully, but there are also many unit tests that need to be updated to work with these changes. Please review those unit test changes in the Azure DevOps commit for this lesson.

Conclusion

At the end of this chapter, we created another Pull Request to merge our “chapter-4” branch changes into the main branch. For a refresher on how to create and complete a Git Pull Request, please jump back to Lesson 1.11.

Also providing another reminder that we can go back to each individual commit per lesson to look at the code changes and the corresponding tests per lesson. Please look back at Lesson 2.11 to review how to view specific commits in Azure DevOps.

In conclusion, we’ve enhanced our game engine with more complex functionality and deeper combat. As we move into the next chapter, we are going to start looking at hosting our Blazor application in Azure and building webservices that provide the game data, so that we can build a richer decoupled game world. We will learn about some basics of Azure compute and storage with a focus on Serverless technologies and Azure Functions.

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