After adding in a lot of new code, it’s a good to do some refactoring and cleanup. This lesson will help us keep our code easier to work with in the future.
Lesson Steps
Step 1: Edit Engine\ViewModels\GameSesssion.cs
When we added the HasMonster property, we didn’t use a getter and setter. Instead, the value of that property is determined by checking if the CurrentMonster property is null.
We’ll do the same with the four “HasLocation” properties (on lines 59 through 69 below), and replace the “get” and “return” with “=>”.
This doesn’t change the code. But, it is a little easier to read – which will help us.
GameSession.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | using System; using System.Linq; using Engine.EventArgs; using Engine.Factories; using Engine.Models; namespace Engine.ViewModels { public class GameSession : BaseNotificationClass { public event EventHandler<GameMessageEventArgs> OnMessageRaised; #region Properties private Location _currentLocation; private Monster _currentMonster; public World CurrentWorld { get; set; } public Player CurrentPlayer { get; set; } public Location CurrentLocation { get { return _currentLocation; } set { _currentLocation = value; OnPropertyChanged(nameof(CurrentLocation)); OnPropertyChanged(nameof(HasLocationToNorth)); OnPropertyChanged(nameof(HasLocationToEast)); OnPropertyChanged(nameof(HasLocationToWest)); OnPropertyChanged(nameof(HasLocationToSouth)); GivePlayerQuestsAtLocation(); GetMonsterAtLocation(); } } public Monster CurrentMonster { get { return _currentMonster; } set { _currentMonster = value; OnPropertyChanged(nameof(CurrentMonster)); OnPropertyChanged(nameof(HasMonster)); if (CurrentMonster != null) { RaiseMessage(""); RaiseMessage($"You see a {CurrentMonster.Name} here!"); } } } public Weapon CurrentWeapon { get; set; } public bool HasLocationToNorth => CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1) != null; public bool HasLocationToEast => CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate) != null; public bool HasLocationToSouth => CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1) != null; public bool HasLocationToWest => CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate) != null; public bool HasMonster => CurrentMonster != null; #endregion public GameSession() { CurrentPlayer = new Player { Name = "Scott", CharacterClass = "Fighter", HitPoints = 10, Gold = 1000000, ExperiencePoints = 0, Level = 1 }; if (!CurrentPlayer.Weapons.Any()) { CurrentPlayer.AddItemToInventory(ItemFactory.CreateGameItem(1001)); } CurrentWorld = WorldFactory.CreateWorld(); CurrentLocation = CurrentWorld.LocationAt(0, 0); } public void MoveNorth() { if(HasLocationToNorth) { CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate + 1); } } public void MoveEast() { if(HasLocationToEast) { CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate + 1, CurrentLocation.YCoordinate); } } public void MoveSouth() { if(HasLocationToSouth) { CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate, CurrentLocation.YCoordinate - 1); } } public void MoveWest() { if(HasLocationToWest) { CurrentLocation = CurrentWorld.LocationAt(CurrentLocation.XCoordinate - 1, CurrentLocation.YCoordinate); } } private void GivePlayerQuestsAtLocation() { foreach(Quest quest in CurrentLocation.QuestsAvailableHere) { if(!CurrentPlayer.Quests.Any(q => q.PlayerQuest.ID == quest.ID)) { CurrentPlayer.Quests.Add(new QuestStatus(quest)); } } } private void GetMonsterAtLocation() { CurrentMonster = CurrentLocation.GetMonster(); } public void AttackCurrentMonster() { if (CurrentWeapon == null) { RaiseMessage("You must select a weapon, to attack."); return; } // Determine damage to monster int damageToMonster = RandomNumberGenerator.NumberBetween(CurrentWeapon.MinimumDamage, CurrentWeapon.MaximumDamage); if (damageToMonster == 0) { RaiseMessage($"You missed the {CurrentMonster.Name}."); } else { CurrentMonster.HitPoints -= damageToMonster; RaiseMessage($"You hit the {CurrentMonster.Name} for {damageToMonster} points."); } // If monster if killed, collect rewards and loot if(CurrentMonster.HitPoints <= 0) { RaiseMessage(""); RaiseMessage($"You defeated the {CurrentMonster.Name}!"); CurrentPlayer.ExperiencePoints += CurrentMonster.RewardExperiencePoints; RaiseMessage($"You receive {CurrentMonster.RewardExperiencePoints} experience points."); CurrentPlayer.Gold += CurrentMonster.RewardGold; RaiseMessage($"You receive {CurrentMonster.RewardGold} gold."); foreach(ItemQuantity itemQuantity in CurrentMonster.Inventory) { GameItem item = ItemFactory.CreateGameItem(itemQuantity.ItemID); CurrentPlayer.AddItemToInventory(item); RaiseMessage($"You receive {itemQuantity.Quantity} {item.Name}."); } // Get another monster to fight GetMonsterAtLocation(); } else { // If monster is still alive, let the monster attack int damageToPlayer = RandomNumberGenerator.NumberBetween(CurrentMonster.MinimumDamage, CurrentMonster.MaximumDamage); if (damageToPlayer == 0) { RaiseMessage("The monster attacks, but misses you."); } else { CurrentPlayer.HitPoints -= damageToPlayer; RaiseMessage($"The {CurrentMonster.Name} hit you for {damageToPlayer} points."); } // If player is killed, move them back to their home. if (CurrentPlayer.HitPoints <= 0) { RaiseMessage(""); RaiseMessage($"The {CurrentMonster.Name} killed you."); CurrentLocation = CurrentWorld.LocationAt(0, -1); // Player's home CurrentPlayer.HitPoints = CurrentPlayer.Level * 10; // Completely heal the player } } } private void RaiseMessage(string message) { OnMessageRaised?.Invoke(this, new GameMessageEventArgs(message)); } } } |
Step 2: Edit Engine\Factories\ItemFactory.cs
In the current code, we initialize _standardGameItems inside the constructor. However, we can do that where we declare it, on line 9, and remove the initialization from the constructor.
I’ve also set _standardGameItems as “readonly”. We don’t need to ever set this to a new list – we only ever add to the initial list. So, we can make the code more restrictive by making the variable readonly (where you can only set it once).
It’s usually a good idea to make your code as restrictive as possible. This prevents you from accidentally doing something later that you didn’t want to do.
ItemFactory.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | using System.Collections.Generic; using System.Linq; using Engine.Models; namespace Engine.Factories { public static class ItemFactory { private static readonly List<GameItem> _standardGameItems = new List<GameItem>(); static ItemFactory() { _standardGameItems.Add(new Weapon(1001, "Pointy Stick", 1, 1, 2)); _standardGameItems.Add(new Weapon(1002, "Rusty Sword", 5, 1, 3)); _standardGameItems.Add(new GameItem(9001, "Snake fang", 1)); _standardGameItems.Add(new GameItem(9002, "Snakeskin", 2)); _standardGameItems.Add(new GameItem(9003, "Rat tail", 1)); _standardGameItems.Add(new GameItem(9004, "Rat fur", 2)); _standardGameItems.Add(new GameItem(9005, "Spider fang", 1)); _standardGameItems.Add(new GameItem(9006, "Spider silk", 2)); } public static GameItem CreateGameItem(int itemTypeID) { GameItem standardItem = _standardGameItems.FirstOrDefault(item => item.ItemTypeID == itemTypeID); if (standardItem != null) { if (standardItem is Weapon) { return (standardItem as Weapon).Clone(); } return standardItem.Clone(); } return null; } } } |
Step 3: Edit Engine\Factories\WorldFactory.cs and Engine\Models\World.cs
When we create each location, we pass in the same string “/Engine;component/Images/Locations/” for each location’s image. We can move that to the AddLocation function in the World class.
You generally don’t want to repeat code, including values. If you add more locations to the game, it would be easy to accidentally mistype this information. So, it’s safer to move it to one place (like we do in the Monster class).
In WorldFactory.cs, remove that string from all the calls to newWorld.AddLocation.
Then, in World.cs, change line 20 to include the common string.
We’ll also make the _locations list readonly, like we did with _standardGameItems in ItemFactory.cs (in step 2).
WorldFactory.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | using Engine.Models; namespace Engine.Factories { internal static class WorldFactory { internal static World CreateWorld() { World newWorld = new World(); newWorld.AddLocation(-2, -1, "Farmer's Field", "There are rows of corn growing here, with giant rats hiding between them.", "FarmFields.png"); newWorld.LocationAt(-2, -1).AddMonster(2, 100); newWorld.AddLocation(-1, -1, "Farmer's House", "This is the house of your neighbor, Farmer Ted.", "Farmhouse.png"); newWorld.AddLocation(0, -1, "Home", "This is your home", "Home.png"); newWorld.AddLocation(-1, 0, "Trading Shop", "The shop of Susan, the trader.", "Trader.png"); newWorld.AddLocation(0, 0, "Town square", "You see a fountain here.", "TownSquare.png"); newWorld.AddLocation(1, 0, "Town Gate", "There is a gate here, protecting the town from giant spiders.", "TownGate.png"); newWorld.AddLocation(2, 0, "Spider Forest", "The trees in this forest are covered with spider webs.", "SpiderForest.png"); newWorld.LocationAt(2, 0).AddMonster(3, 100); newWorld.AddLocation(0, 1, "Herbalist's hut", "You see a small hut, with plants drying from the roof.", "HerbalistsHut.png"); newWorld.LocationAt(0, 1).QuestsAvailableHere.Add(QuestFactory.GetQuestByID(1)); newWorld.AddLocation(0, 2, "Herbalist's garden", "There are many plants here, with snakes hiding behind them.", "HerbalistsGarden.png"); newWorld.LocationAt(0, 2).AddMonster(1, 100); return newWorld; } } } |
World.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Engine.Models { public class World { private readonly List<Location> _locations = new List<Location>(); internal void AddLocation(int xCoordinate, int yCoordinate, string name, string description, string imageName) { Location loc = new Location(); loc.XCoordinate = xCoordinate; loc.YCoordinate = yCoordinate; loc.Name = name; loc.Description = description; loc.ImageName = $"/Engine;component/Images/Locations/{imageName}"; _locations.Add(loc); } public Location LocationAt(int xCoordinate, int yCoordinate) { foreach(Location loc in _locations) { if(loc.XCoordinate == xCoordinate && loc.YCoordinate == yCoordinate) { return loc; } } return null; } } } |
Step 4: Edit Engine\Models\Monster.cs
In World.cs, we used “string interpolation” to combine the image file name with its resource location information. So, let’s stay consistent, and make the same change to line 35 of Monster.cs.
Monster.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | using System.Collections.ObjectModel; namespace Engine.Models { public class Monster : BaseNotificationClass { private int _hitPoints; public string Name { get; private set; } public string ImageName { get; set; } public int MaximumHitPoints { get; private set; } public int HitPoints { get { return _hitPoints; } set { _hitPoints = value; OnPropertyChanged(nameof(HitPoints)); } } public int MinimumDamage { get; set; } public int MaximumDamage { get; set; } public int RewardExperiencePoints { get; private set; } public int RewardGold { get; private set; } public ObservableCollection<ItemQuantity> Inventory { get; set; } public Monster(string name, string imageName, int maximumHitPoints, int hitPoints, int minimumDamage, int maxmumDamage, int rewardExperiencePoints, int rewardGold) { Name = name; ImageName = $"/Engine;component/Images/Monsters/{imageName}"; MaximumHitPoints = maximumHitPoints; HitPoints = hitPoints; MinimumDamage = minimumDamage; MaximumDamage = maxmumDamage; RewardExperiencePoints = rewardExperiencePoints; RewardGold = rewardGold; Inventory = new ObservableCollection<ItemQuantity>(); } } } |
Step 5: Edit WPFUI\MainWindow.xaml.cs
See that private _gameSession variable on line 13? It’s only set once, in the constructor. So, that’s another variable we can make readonly.
We can also initialize it on line 13, and remove the line that was initializing the variable in the constructor.
MainWindow.xaml.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | using System.Windows; using System.Windows.Documents; using Engine.EventArgs; using Engine.ViewModels; namespace WPFUI { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private readonly GameSession _gameSession = new GameSession(); public MainWindow() { InitializeComponent(); _gameSession.OnMessageRaised += OnGameMessageRaised; DataContext = _gameSession; } private void OnClick_MoveNorth(object sender, RoutedEventArgs e) { _gameSession.MoveNorth(); } private void OnClick_MoveWest(object sender, RoutedEventArgs e) { _gameSession.MoveWest(); } private void OnClick_MoveEast(object sender, RoutedEventArgs e) { _gameSession.MoveEast(); } private void OnClick_MoveSouth(object sender, RoutedEventArgs e) { _gameSession.MoveSouth(); } private void OnClick_AttackMonster(object sender, RoutedEventArgs e) { _gameSession.AttackCurrentMonster(); } private void OnGameMessageRaised(object sender, GameMessageEventArgs e) { GameMessages.Document.Blocks.Add(new Paragraph(new Run(e.Message))); GameMessages.ScrollToEnd(); } } } |
Step 7: Test the game, to make sure it still works.