📜 ⬆️ ⬇️

We write game logic in C #. Part 2/2

This is a continuation of the previous article . Step by step, we are creating an engine on which the game logic of our economic strategy will work. If you see this for the first time, I strongly recommend starting with Part 1 , as this is a dependent continuation and requires its context.

As before - at the bottom of the article you can find the full code for the GitHab and a link to free download.




')

Work plan


1. Set up projects
2. Create a core (basic facilities)
3. Add and test the first commands - build the structure and module
4. We take out the settings of buildings and modules in a separate file.
5. Add the flow of time
6. Add Constructible, buildings are now under construction for some time.
7. Add resources, for the construction of the necessary resources
8. Add a production cycle - the module consumes and issues resources.

Adding Constructible


Let's tie something to the flow of time now. Let the buildings and modules are not built immediately, but several moves (depending on the configuration). To begin with, we will add the ConstructionTime item to all settings. If ConstructionTime is zero, the structure cannot be built.

public class BuildingConfig { // ... public int ConstructionTime; } 
 public class ModuleConfig { // ... public int ConstructionTime; } 

Do not forget to add the settings to the factory:

 public class Factory { // ... Type = BuildingType.PowerPlant, ConstructionTime = 8, // ... Type = BuildingType.Smeltery, ConstructionTime = 10, // ... Type = BuildingType.Roboport, ConstructionTime = 12, // ... Type = ModuleType.Generator, ConstructionTime = 5 // ... Type = ModuleType.Furnace, ConstructionTime = 6 // ... Type = ModuleType.Digger, ConstructionTime = 7 // ... Type = ModuleType.Miner, ConstructionTime = 8 // ... } 

Now create a class Progression, which we will implement any progressions that flow in time, for example, construction.

 public class Progression { public readonly int Time; public int Progress { get; private set; } public bool IsFake { get { return Time == 0; } } public bool IsReady { get { return IsFake || Progress >= Time; } } public bool IsRunning { get { return !IsReady && Progress > 0; } } public Progression (int time) { Time = time; Progress = 0; } public void AddProgress () { if (!IsReady) Progress++; } public void Complete () { if (!IsReady) Progress = Time; } public void Reset () { Progress = 0; } } 

Now we add the possibility of construction in our rooms and modules.

 public class Building { // ... public readonly Progression Constructible; // ... public Building (BuildingConfig config) { // ... Constructible = new Progression(config.ConstructionTime); } 

 public class Module { // ... public readonly Progression Constructible; public Module (ModuleConfig config) { // ... Constructible = new Progression(config.ConstructionTime); } 

And we prohibit the construction of modules in a room not yet built:

 public class ModuleConstruct : Command { // ... protected override bool Run () { // ... if (!Building.Constructible.IsReady) { return false; } 

Of course, tests fell after that, so we will add CorrectConstruction, IncorrectConstruction, CantConstructInWrongBuilding and ModulesLimits to tests after successfully completing the BuildingConstruct command, calling the Complete method (yes, we created it for this purpose)

 room.Building.Constructible.Complete() 

And to test the inability to build in an unfinished room we will write a separate test:

 [TestMethod] public void CantConstructInUncompleteBuilding () { var core = new GameLogic.Core(); var room = core.Ship.GetRoom(0); new BuildingConstruct( room, core.Factory.ProduceBuilding(BuildingType.PowerPlant) ) .Execute(core); Assert.IsFalse( new ModuleConstruct( room.Building, core.Factory.ProduceModule(ModuleType.Generator), 2 ) .Execute(core) .IsValid ); } 

But now let's make the room build not only by the wave of the hand of the gods of the world of our game, but simply with time. To do this, create a special command and call it every turn:
 public class NextTurn : Command { protected override bool Run () { new ConstructionProgress().Execute(Core); // .. } } 

 public class ConstructionProgress : Command { protected override bool Run () { foreach (var room in Core.Ship.Rooms) { BuildingProgress(room.Building); } return true; } private void BuildingProgress (Building building) { building.Constructible.AddProgress(); foreach (var module in building.Modules) { module.Constructible.AddProgress(); } } } 

And immediately we will cover the tests that show that the code works great:
 [TestMethod] public void Constructible () { const int smelteryTime = 10; const int furnaceTime = 6; var core = new GameLogic.Core(); var room = core.Ship.GetRoom(0); // Smeltery new BuildingConstruct( room, core.Factory.ProduceBuilding(BuildingType.Smeltery) ) .Execute(core); Assert.IsFalse( room.Building.Constructible.IsReady ); new NextTurnCount(smelteryTime - 1).Execute(core); Assert.IsFalse(room.Building.Constructible.IsReady); new NextTurn().Execute(core); Assert.IsTrue(room.Building.Constructible.IsReady); // Furnace new ModuleConstruct( room.Building, core.Factory.ProduceModule(ModuleType.Furnace), 2 ).Execute(core); var module = room.Building.GetModule(2); Assert.IsFalse( module.Constructible.IsReady ); new NextTurnCount(furnaceTime - 1).Execute(core); Assert.IsFalse(module.Constructible.IsReady); new NextTurn().Execute(core); Assert.IsTrue(module.Constructible.IsReady); } 



Add resources


In order to create something you first need to destroy something and collect scrap metal. Let's sell resources so that the player has to pay for his buildings. There will be three resources - Energy, Ore and Metal.

 public enum ResourceType { Energy, Ore, Metal } 

We will also create a bank where the player will store and where to take resources from.

 public class Bank { private readonly Dictionary<ResourceType, int> resources = new Dictionary<ResourceType, int>(); public int Get (ResourceType type) { return resources.ContainsKey(type) ? resources[type] : 0; } public void Change (ResourceType type, int value) { var current = Get(type); if (current + value < 0) { throw new ArgumentOutOfRangeException("Not enought " + type + " in bank"); } resources[type] = current + value; } } 

 public class Core { // ... public readonly Bank Bank = new Bank(); } 

Now we add the price of production to the settings of the modules and buildings:

 public class BuildingConfig { // ... public Dictionary<ResourceType, int> ConstructionCost; } 

 public class ModuleConfig { // ... public Dictionary<ResourceType, int> ConstructionCost; } 

 public class Factory { // ... Type = BuildingType.PowerPlant, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 20 }}, // ... Type = BuildingType.Smeltery, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 20 }}, // ... Type = BuildingType.Roboport, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 20 }}, // ... // ... Type = ModuleType.Generator, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 10 }}, // ... Type = ModuleType.Furnace, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 10 }}, // ... Type = ModuleType.Digger, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 10 }}, // ... Type = ModuleType.Miner, ConstructionCost = new Dictionary<ResourceType, int>() {{ ResourceType.Metal, 40 }}, // ... } 

Now we will add a team that allows you to pay resources and immediately try it in (in tests):

 public class Pay : Command { public readonly Dictionary<ResourceType, int> Cost; public Pay (Dictionary<ResourceType, int> cost) { Cost = cost; } protected override bool Run () { //        -       if (Cost.Any(item => Core.Bank.Get(item.Key) < item.Value)) { return false; } //    -    foreach (var item in Cost) { Core.Bank.Change(item.Key, -item.Value); } return true; } } 

 [TestClass] public class Player { [TestMethod] public void Payment () { var core = new Core(); core.Bank.Change(ResourceType.Metal, 100); core.Bank.Change(ResourceType.Ore, 150); Assert.IsFalse( new Pay(new Dictionary<ResourceType, int>{ { ResourceType.Metal, 100 }, { ResourceType.Ore, 2000 } }) .Execute(core) .IsValid ); Assert.AreEqual(100, core.Bank.Get(ResourceType.Metal)); Assert.AreEqual(150, core.Bank.Get(ResourceType.Ore)); Assert.IsTrue( new Pay(new Dictionary<ResourceType, int>{ { ResourceType.Metal, 100 }, { ResourceType.Ore, 30 } }) .Execute(core) .IsValid ); Assert.AreEqual(0, core.Bank.Get(ResourceType.Metal)); Assert.AreEqual(120, core.Bank.Get(ResourceType.Ore)); } } 

Payment works correctly and start paying for buildings and modules is quite simple - we will add a call to the Pay command as the last validation (it should be the last if we don’t want another check to prevent the construction from being made after payment):

 public class BuildingConstruct : Command { // ... protected override bool Run () { // ... if (!new Pay(Building.Config.ConstructionCost).Execute(Core).IsValid) { return false; } Room.Building = Building; return true; } } 

 public class ModuleConstruct : Command { // ... protected override bool Run () { // ... if (!new Pay(Module.Config.ConstructionCost).Execute(Core).IsValid) { return false; } Building.SetModule(Position, module); return true; } } 

Fortunately, the tests fell off again (fortunately, because it means that they do their job well).

In the old tests we will add resources to the player and write a new test, which in the future will check that suddenly there was no opportunity to build a structure for free. Add to all broken tests closer to the beginning:

 core.Bank.Change(ResourceType.Metal, 1000); 

And we write a test for the construction with a shortage of resources:
 [TestMethod] public void CantBuiltCostly () { var core = new GameLogic.Core(); var room = core.Ship.GetRoom(0); core.Bank.Change(ResourceType.Metal, 3); Assert.IsFalse( new BuildingConstruct( room, core.Factory.ProduceBuilding(BuildingType.Smeltery) ) .Execute(core) .IsValid ); } 



Add production cycle


Of course, it is pleasant to take resources, but it is much more pleasant to give. Let's set the ability to run production chains. Each module will be able to eat a certain amount of raw materials and then produce the finished material. Start again with the configuration:

 public class ModuleConfig { // ... public int CycleTime; //       public Dictionary<ResourceType, int> CycleInput; //   public Dictionary<ResourceType, int> CycleOutput; //     } 

 public class Module { // ... public readonly Progression Cycle; public Module (ModuleConfig config) { // ... Cycle = new Progression(config.CycleTime); } } 

 public class Factory { // ... { ModuleType.Generator, new ModuleConfig() { // ... CycleTime = 12, CycleInput = null, //    ,   CycleOutput = new Dictionary<ResourceType, int>() { { ResourceType.Energy, 10 } }, }}, { ModuleType.Furnace , new ModuleConfig() { // ... CycleTime = 16, CycleInput = new Dictionary<ResourceType, int>() { { ResourceType.Energy, 6 }, { ResourceType.Ore, 4 }, }, CycleOutput = new Dictionary<ResourceType, int>() { { ResourceType.Metal, 5 } } }}, { ModuleType.Digger , new ModuleConfig() { // ... CycleTime = 18, CycleInput = new Dictionary<ResourceType, int>() { { ResourceType.Energy, 2 } }, CycleOutput = new Dictionary<ResourceType, int>() { { ResourceType.Ore, 7 } } }}, { ModuleType.Miner , new ModuleConfig() { // ... CycleTime = 32, CycleInput = new Dictionary<ResourceType, int>() { { ResourceType.Energy, 8 } }, CycleOutput = new Dictionary<ResourceType, int>() { { ResourceType.Ore, 40 } } }} 

Now add to each move the progress of production:

 public class NextTurn : Command { protected override bool Run () { new CycleProgress().Execute(Core); //    ,      // ... } } 

 public class CycleProgress : Command { protected override bool Run () { foreach (var room in Core.Ship.Rooms) { BuildingProgress(room.Building); } return true; } private void BuildingProgress (Building building) { if (!building.Constructible.IsReady) return; foreach (var module in building.Modules) { ModuleProgress(module); } } private void ModuleProgress (Module module) { if (!module.Constructible.IsReady || module.Cycle.IsFake) { return; } //        (  ) //        ( ) if (module.Cycle.IsRunning || TryStartCycle(module)) { AddStep(module); } } private void AddStep (Module module) { module.Cycle.AddProgress(); //       ... if (module.Cycle.IsReady) { // ...     CycleOutput(module); // ...   ,       module.Cycle.Reset(); } } private bool TryStartCycle (Module module) { if (module.Config.CycleInput == null) { return true; } //       -   return new Pay(module.Config.CycleInput).Execute(Core).IsValid; } private void CycleOutput (Module module) { foreach (var item in module.Config.CycleOutput) { //    ,     Core.Bank.Change(item.Key, item.Value); } } } 

The class turned out to be large, but we can always refactor if the complexity is too high. Now we write the test. It will be quite long, and check the correctness of production, and non-launch in case of shortage of resources. I also created separate settings for the module and the structure specifically for the test (all of a sudden the DG will change them and my tests will drop). Ideally, all the tests could be changed to special test settings:

 public class Cycle { [TestMethod] public void CheckCycle () { var buildingConfig = new BuildingConfig() { Type = BuildingType.Smeltery, ModulesLimit = 1, AvailableModules = new [] { ModuleType.Furnace } }; var moduleConfig = new ModuleConfig() { Type = ModuleType.Furnace, ConstructionTime = 2, ConstructionCost = new Dictionary<ResourceType, int>() { { ResourceType.Metal, 10 } }, CycleTime = 4, CycleInput = new Dictionary<ResourceType, int>() { { ResourceType.Ore, 10 }, { ResourceType.Energy, 5 } }, CycleOutput = new Dictionary<ResourceType, int>() { { ResourceType.Metal, 1 } } }; var core = new Core(); core.Bank.Change(ResourceType.Metal, 10); core.Bank.Change(ResourceType.Ore, 80); core.Bank.Change(ResourceType.Energy, 10); var building = new Building(buildingConfig); core.Ship.GetRoom(0).Building = building; var module = new Module(moduleConfig); Assert.IsTrue( new ModuleConstruct(building, module, 0) .Execute(core) .IsValid ); new NextTurn().Execute(core); Assert.IsFalse(module.Cycle.IsRunning); new NextTurn().Execute(core); Assert.IsTrue(module.Constructible.IsReady); Assert.IsFalse(module.Cycle.IsRunning); new NextTurn().Execute(core); Assert.IsTrue(module.Cycle.IsRunning); Assert.AreEqual(1, module.Cycle.Progress); Assert.AreEqual(70, core.Bank.Get(ResourceType.Ore)); Assert.AreEqual(5, core.Bank.Get(ResourceType.Energy)); Assert.AreEqual(0, core.Bank.Get(ResourceType.Metal)); new NextTurnCount(3).Execute(core); Assert.IsFalse(module.Cycle.IsRunning); Assert.AreEqual(70, core.Bank.Get(ResourceType.Ore)); Assert.AreEqual(5, core.Bank.Get(ResourceType.Energy)); Assert.AreEqual(1, core.Bank.Get(ResourceType.Metal)); new NextTurn().Execute(core); Assert.IsTrue(module.Cycle.IsRunning); Assert.AreEqual(60, core.Bank.Get(ResourceType.Ore)); Assert.AreEqual(0, core.Bank.Get(ResourceType.Energy)); Assert.AreEqual(1, core.Bank.Get(ResourceType.Metal)); new NextTurnCount(3).Execute(core); Assert.IsFalse(module.Cycle.IsRunning); Assert.AreEqual(2, core.Bank.Get(ResourceType.Metal)); new NextTurn().Execute(core); // Cant launch because of Energy leak Assert.IsFalse(module.Cycle.IsRunning); Assert.AreEqual(60, core.Bank.Get(ResourceType.Ore)); Assert.AreEqual(0, core.Bank.Get(ResourceType.Energy)); } } 



the end


So, the tests started correctly and we were able to make the minimum version of our product. The Factory class has turned out to be bloated, but if you make the settings in JSON, then it will be quite nothing. Using Json.NET we need to write something like this:

JSON Settings
 var files = Directory.GetFiles(path + "/Items/Modules", "*.json", SearchOption.AllDirectories); var modules = new List<ModuleConfig>(); foreach (var file in modules) { var content = File.ReadAllText(file); modules.Add( JsonConvert.DeserializeObject<ModuleConfig>(content) ); } 


 { "Type": "Generator", "ConstructionTime": 5, "ConstructionCost": { "Metal": 10 }, "CycleTime": 12, "CycleInput": { "Energy" 6, "Ore": 4, }, "CycleOutput": { "Energy": 10 } } 


For those who just love the code - there is a separate repository on GitHub.

In addition, if you are interested in questions on the development of SpaceLab - ask, I will answer them in the comments or in a separate article

Download for Windows, Linux, Mac for free and without SMS, as well as you can support us on the SpaceLab page at GreenLight

Source: https://habr.com/ru/post/322268/


All Articles