Repository: jbradberry/universe

Issue:
Inhabited planets can have planetary facilities
In order for anything to be constructed in this game, as in Stars, minerals need to be mined from planets and population needs to produce action resources which can be enhanced by a special type of planetary facility called factories.

- [x] species get a new parameter determining how many action resources per population planets get per year
- [x] species get new parameters determining how many resources a mine costs, how many mines can be operated by a given quantity of population, and the maximum minerals a mine can produce per year
- [x] add a function that calculates a planet's resource production
- [x] add a function that calculates a planet's mineral production
- [x] species get new parameters determining factory costs in action resources plus germanium, how many factories can be operated by a given quantity of population, and how many action resources an operated factory adds to a planet's productivity
- [x] add a component to planets that has the counts of mines and factories on the planet
- [x] add a new system that calculates and adds new mineral quantities to the planet surface based on the mines, population, and the mineral concentrations of an inhabited planet
- ~~decrease the mineral concentrations as they are mined~~
  - split off into #39
- ~~mines may be queued and built using nothing more than action resources~~
  - split off into #38
- ~~add a new system that builds these facilities, decreasing the relevant resources, stopping work when all resources are exhausted for the year, and leaving partially completed items in the queue to be worked on in subsequent turns~~
  - split off into #38

Discussion:

Patch:
diff --git a/universe/components.py b/universe/components.py
index c9dbf3a..e171be0 100644
--- a/universe/components.py
+++ b/universe/components.py
@@ -72,6 +72,10 @@ class SpeciesComponent(Component):
     plural_name = fields.CharField(required=True)
     growth_rate = fields.IntField(required=True)
 
+
+class SpeciesEnvironmentComponent(Component):
+    _name = 'species_environment'
+
     gravity_min = fields.IntField(min=0, max=100, required=False)
     gravity_max = fields.IntField(min=0, max=100, required=False)
     gravity_immune = fields.BooleanField()
@@ -115,6 +119,22 @@ class SpeciesComponent(Component):
                     "'radiation_min' and 'radiation_max' must be set if 'radiation_immune' is false.")
 
 
+class SpeciesProductionComponent(Component):
+    _name = 'species_production'
+
+    population_per_r = fields.IntField(min=700)
+
+    factories_produce_r = fields.IntField(min=5, max=15)  # each 10 factories produce X production
+    factories_cost_r = fields.IntField(min=5, max=25)  # each factory costs X production
+    factories_per_pop = fields.IntField(min=5, max=25)  # each 10k colonists can operate X factories
+
+    factories_cost_less = fields.BooleanField()  # each factory costs 3kT germanium instead of 4kT
+
+    minerals_per_m = fields.IntField(min=5, max=25)  # each 10 mines produce X kT of minerals
+    mines_cost_r = fields.IntField(min=2, max=15)  # each mine costs X production
+    mines_per_pop = fields.IntField(min=5, max=25)  # each 10k colonists can operate X mines
+
+
 class OwnershipComponent(Component):
     _name = 'ownership'
 
@@ -124,7 +144,7 @@ class OwnershipComponent(Component):
 class PopulationComponent(Component):
     _name = 'population'
 
-    population = fields.IntField(required=False)
+    population = fields.IntField(min=0, required=False)
 
 
 class EnvironmentComponent(Component):
@@ -181,6 +201,13 @@ class MineralInventoryComponent(Component):
     germanium = fields.IntField(min=0, required=False)
 
 
+class PlanetaryFacilitiesComponent(Component):
+    _name = 'facilities'
+
+    mines = fields.IntField(min=0, required=False)
+    factories = fields.IntField(min=0, required=False)
+
+
 class OrderComponent(Component):
     _name = 'orders'
 
diff --git a/universe/engine.py b/universe/engine.py
index 83ffd4a..6b0ab2f 100644
--- a/universe/engine.py
+++ b/universe/engine.py
@@ -144,8 +144,14 @@ class GameState:
         self.manager = Manager()
         self.manager.register_system(systems.UpdateSystem)
         self.manager.register_system(systems.MovementSystem)
+        self.manager.register_system(systems.MiningSystem)
         self.manager.register_system(systems.PopulationGrowthSystem)
 
+        self.manager.register_entity_type('species', [
+            components.SpeciesComponent(),
+            components.SpeciesEnvironmentComponent(),
+            components.SpeciesProductionComponent(),
+        ])
         self.manager.register_entity_type('ship', [
             components.PositionComponent(),
             components.OwnershipComponent(),
@@ -157,12 +163,10 @@ class GameState:
             components.EnvironmentComponent(),
             components.MineralConcentrationComponent(),
             components.MineralInventoryComponent(),
+            components.PlanetaryFacilitiesComponent(),
             components.OwnershipComponent(),
             components.PopulationComponent(),
         ])
-        self.manager.register_entity_type('species', [
-            components.SpeciesComponent(),
-        ])
         self.manager.register_entity_type('movement_order', [
             components.OrderComponent(),
             components.MovementComponent(),
diff --git a/universe/systems.py b/universe/systems.py
index 7383d01..7dc72b4 100644
--- a/universe/systems.py
+++ b/universe/systems.py
@@ -138,6 +138,19 @@ class MovementSystem:
                     order.seq = i
 
 
+class MiningSystem:
+    def process(self, manager):
+        for _id, entity in manager.get_entities('mineral_concentrations').items():
+            species = manager.get_entity('species', entity.owner_id)
+            if species is None:
+                continue
+
+            ir, bo, ge = utils.mining(species, entity)
+            entity.ironium = (entity.ironium or 0) + ir
+            entity.boranium = (entity.boranium or 0) + bo
+            entity.germanium = (entity.germanium or 0) + ge
+
+
 class PopulationGrowthSystem:
     def process(self, manager):
         for _id, entity in manager.get_entities('population').items():
diff --git a/universe/utils.py b/universe/utils.py
index 0efb57b..ae54344 100644
--- a/universe/utils.py
+++ b/universe/utils.py
@@ -1,3 +1,4 @@
+from decimal import Decimal
 import math
 
 
@@ -25,3 +26,23 @@ def planet_value(species, planet):
     if red != 0:
         return -red
     return int(int(math.sqrt(value / 3) + 0.9) * ideal / 10000)
+
+
+def production(species, planet):
+    population = Decimal(planet.population or 0)
+
+    return int(population / species.population_per_r)
+
+
+def mining(species, planet):
+    population = Decimal(planet.population or 0)
+    mines = planet.mines or 0
+    can_operate = int(population / 10_000) * species.mines_per_pop
+
+    mines = Decimal(min(mines, can_operate))
+    capacity = int(mines * species.minerals_per_m / 10)
+    ir = Decimal(planet.ironium_conc) / 100
+    bo = Decimal(planet.boranium_conc) / 100
+    ga = Decimal(planet.germanium_conc) / 100
+
+    return (int(ir * capacity), int(bo * capacity), int(ga * capacity))


Test patch:
diff --git a/tests/test_components.py b/tests/test_components.py
index 5e819ef..6deafd5 100644
--- a/tests/test_components.py
+++ b/tests/test_components.py
@@ -122,7 +122,7 @@ class MineralConcentrationComponentTestCase(unittest.TestCase):
         self.assertTrue(components.MineralConcentrationComponent().serialize(data))
 
 
-class SpeciesComponentTestCase(unittest.TestCase):
+class SpeciesEnvironmentComponentTestCase(unittest.TestCase):
     def test_immune(self):
         data = {
             'type': 'species',
@@ -133,12 +133,12 @@ class SpeciesComponentTestCase(unittest.TestCase):
             'temperature_immune': True,
             'radiation_immune': True,
         }
-        component = components.SpeciesComponent()
+        component = components.SpeciesEnvironmentComponent()
 
         self.assertIsNone(component.validate(data))
 
     def test_immune_with_value(self):
-        component = components.SpeciesComponent()
+        component = components.SpeciesEnvironmentComponent()
 
         for fname in ('gravity', 'temperature', 'radiation'):
             data = {
@@ -171,13 +171,13 @@ class SpeciesComponentTestCase(unittest.TestCase):
             'temperature_immune': False,
             'radiation_immune': False,
         }
-        component = components.SpeciesComponent()
+        component = components.SpeciesEnvironmentComponent()
 
         with self.assertRaises(exceptions.ValidationError):
             component.validate(data)
 
     def test_not_immune_without_value(self):
-        component = components.SpeciesComponent()
+        component = components.SpeciesEnvironmentComponent()
 
         for fname in ('gravity', 'temperature', 'radiation'):
             data = {
diff --git a/tests/test_engine.py b/tests/test_engine.py
index 24e7dd9..2fafa0e 100644
--- a/tests/test_engine.py
+++ b/tests/test_engine.py
@@ -57,6 +57,14 @@ class PersistenceTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {
                     'pk': 1,
@@ -112,6 +120,14 @@ class PersistenceTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {
                     'pk': 1,
@@ -155,6 +171,14 @@ class PersistenceTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
             ]
         }
@@ -172,6 +196,14 @@ class PersistenceTestCase(unittest.TestCase):
         self.assertEqual(species['gravity_immune'], True)
         self.assertEqual(species['temperature_immune'], True)
         self.assertEqual(species['radiation_immune'], True)
+        self.assertEqual(species['population_per_r'], 1000)
+        self.assertEqual(species['factories_produce_r'], 10)
+        self.assertEqual(species['factories_cost_r'], 10)
+        self.assertEqual(species['factories_per_pop'], 10)
+        self.assertEqual(species['factories_cost_less'], False)
+        self.assertEqual(species['minerals_per_m'], 10)
+        self.assertEqual(species['mines_cost_r'], 5)
+        self.assertEqual(species['mines_per_pop'], 10)
 
 
 class MovementTestCase(unittest.TestCase):
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 31b6567..aacbcd6 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -60,6 +60,12 @@ class IntFieldTestCase(unittest.TestCase):
 
         self.assertIsNone(field.validate({}))
 
+    def test_ranged_not_required(self):
+        field = fields.IntField(min=0, max=100, required=False)
+        field.name = 'widgets'
+
+        self.assertIsNone(field.validate({}))
+
     def test_explicit_required(self):
         field = fields.IntField(required=True)
         field.name = 'widgets'
diff --git a/tests/test_systems.py b/tests/test_systems.py
index fc098d3..ee98d61 100644
--- a/tests/test_systems.py
+++ b/tests/test_systems.py
@@ -17,6 +17,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
             ]
@@ -52,6 +60,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 2, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8}
@@ -88,6 +104,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 2, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8}
@@ -124,6 +148,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 2, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8},
@@ -163,6 +195,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 2, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8},
@@ -202,6 +242,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 2, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8},
@@ -237,6 +285,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 2, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8},
@@ -272,6 +328,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 2, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8},
@@ -302,6 +366,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 2, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8},
@@ -336,6 +408,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 2, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8},
@@ -371,6 +451,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 1, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 2, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8},
@@ -406,6 +494,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {
                     'pk': 1,
@@ -416,6 +512,14 @@ class UpdateTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {'pk': 2, 'type': 'ship', 'x': 480, 'y': 235, 'owner_id': 0},
                 {'pk': 3, 'type': 'movement_order', 'actor_id': 2, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8},
@@ -439,6 +543,255 @@ class UpdateTestCase(unittest.TestCase):
         )
 
 
+class MiningTestCase(unittest.TestCase):
+    def test_uninhabited(self):
+        state = {
+            'turn': 2500,
+            'width': 1000,
+            'entities': [
+                {
+                    'pk': 0,
+                    'type': 'species',
+                    'name': 'Human',
+                    'plural_name': 'Humans',
+                    'growth_rate': 15,
+                    'gravity_immune': True,
+                    'temperature_immune': True,
+                    'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
+                },
+                {
+                    'pk': 1,
+                    'type': 'planet',
+                    'population': 1000,
+                    'x': 480,
+                    'y': 235,
+                    'gravity': 50,
+                    'temperature': 50,
+                    'radiation': 50,
+                    'ironium_conc': 100,
+                    'boranium_conc': 100,
+                    'germanium_conc': 100,
+                },
+            ]
+        }
+
+        S = engine.GameState(state, {})
+        systems.MiningSystem().process(S.manager)
+
+        planets = S.manager.get_entities('mineral_concentrations')
+        self.assertEqual(len(planets), 1)
+        self.assertEqual(planets[1].ironium or 0, 0)
+        self.assertEqual(planets[1].germanium or 0, 0)
+        self.assertEqual(planets[1].boranium or 0, 0)
+
+    def test_no_mines(self):
+        state = {
+            'turn': 2500,
+            'width': 1000,
+            'entities': [
+                {
+                    'pk': 0,
+                    'type': 'species',
+                    'name': 'Human',
+                    'plural_name': 'Humans',
+                    'growth_rate': 15,
+                    'gravity_immune': True,
+                    'temperature_immune': True,
+                    'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
+                },
+                {
+                    'pk': 1,
+                    'type': 'planet',
+                    'population': 1000,
+                    'x': 480,
+                    'y': 235,
+                    'gravity': 50,
+                    'temperature': 50,
+                    'radiation': 50,
+                    'ironium_conc': 100,
+                    'boranium_conc': 100,
+                    'germanium_conc': 100,
+                    'owner_id': 0,
+                },
+            ]
+        }
+
+        S = engine.GameState(state, {})
+        systems.MiningSystem().process(S.manager)
+
+        planets = S.manager.get_entities('mineral_concentrations')
+        self.assertEqual(len(planets), 1)
+        self.assertEqual(planets[1].ironium or 0, 0)
+        self.assertEqual(planets[1].germanium or 0, 0)
+        self.assertEqual(planets[1].boranium or 0, 0)
+
+    def test_full_concentration(self):
+        state = {
+            'turn': 2500,
+            'width': 1000,
+            'entities': [
+                {
+                    'pk': 0,
+                    'type': 'species',
+                    'name': 'Human',
+                    'plural_name': 'Humans',
+                    'growth_rate': 15,
+                    'gravity_immune': True,
+                    'temperature_immune': True,
+                    'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
+                },
+                {
+                    'pk': 1,
+                    'type': 'planet',
+                    'population': 1_000_000,
+                    'x': 480,
+                    'y': 235,
+                    'gravity': 50,
+                    'temperature': 50,
+                    'radiation': 50,
+                    'ironium_conc': 100,
+                    'boranium_conc': 100,
+                    'germanium_conc': 100,
+                    'mines': 1000,
+                    'owner_id': 0,
+                },
+            ]
+        }
+
+        S = engine.GameState(state, {})
+        systems.MiningSystem().process(S.manager)
+
+        planets = S.manager.get_entities('mineral_concentrations')
+        self.assertEqual(len(planets), 1)
+        self.assertEqual(planets[1].ironium or 0, 1000)
+        self.assertEqual(planets[1].germanium or 0, 1000)
+        self.assertEqual(planets[1].boranium or 0, 1000)
+
+    def test_partial_concentration(self):
+        state = {
+            'turn': 2500,
+            'width': 1000,
+            'entities': [
+                {
+                    'pk': 0,
+                    'type': 'species',
+                    'name': 'Human',
+                    'plural_name': 'Humans',
+                    'growth_rate': 15,
+                    'gravity_immune': True,
+                    'temperature_immune': True,
+                    'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
+                },
+                {
+                    'pk': 1,
+                    'type': 'planet',
+                    'population': 1_000_000,
+                    'x': 480,
+                    'y': 235,
+                    'gravity': 50,
+                    'temperature': 50,
+                    'radiation': 50,
+                    'ironium_conc': 50,
+                    'boranium_conc': 50,
+                    'germanium_conc': 50,
+                    'mines': 1000,
+                    'owner_id': 0,
+                },
+            ]
+        }
+
+        S = engine.GameState(state, {})
+        systems.MiningSystem().process(S.manager)
+
+        planets = S.manager.get_entities('mineral_concentrations')
+        self.assertEqual(len(planets), 1)
+        self.assertEqual(planets[1].ironium or 0, 500)
+        self.assertEqual(planets[1].germanium or 0, 500)
+        self.assertEqual(planets[1].boranium or 0, 500)
+
+    def test_depleted(self):
+        state = {
+            'turn': 2500,
+            'width': 1000,
+            'entities': [
+                {
+                    'pk': 0,
+                    'type': 'species',
+                    'name': 'Human',
+                    'plural_name': 'Humans',
+                    'growth_rate': 15,
+                    'gravity_immune': True,
+                    'temperature_immune': True,
+                    'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
+                },
+                {
+                    'pk': 1,
+                    'type': 'planet',
+                    'population': 1_000_000,
+                    'x': 480,
+                    'y': 235,
+                    'gravity': 50,
+                    'temperature': 50,
+                    'radiation': 50,
+                    'ironium_conc': 1,
+                    'boranium_conc': 1,
+                    'germanium_conc': 1,
+                    'mines': 1000,
+                    'owner_id': 0,
+                },
+            ]
+        }
+
+        S = engine.GameState(state, {})
+        systems.MiningSystem().process(S.manager)
+
+        planets = S.manager.get_entities('mineral_concentrations')
+        self.assertEqual(len(planets), 1)
+        self.assertEqual(planets[1].ironium or 0, 10)
+        self.assertEqual(planets[1].germanium or 0, 10)
+        self.assertEqual(planets[1].boranium or 0, 10)
+
+
 class PopulationGrowthTestCase(unittest.TestCase):
     def test_habitability_growth(self):
         state = {
@@ -454,6 +807,14 @@ class PopulationGrowthTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {
                     'pk': 1,
@@ -493,6 +854,14 @@ class PopulationGrowthTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {
                     'pk': 1,
@@ -541,6 +910,14 @@ class PopulationGrowthTestCase(unittest.TestCase):
                     'temperature_max': 64,
                     'radiation_min': 38,
                     'radiation_max': 90,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {
                     'pk': 1,
@@ -586,6 +963,14 @@ class PopulationGrowthTestCase(unittest.TestCase):
                     'temperature_max': 64,
                     'radiation_min': 38,
                     'radiation_max': 90,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {
                     'pk': 1,
@@ -633,6 +1018,14 @@ class PopulationGrowthTestCase(unittest.TestCase):
                     'temperature_max': 64,
                     'radiation_min': 38,
                     'radiation_max': 90,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {
                     'pk': 1,
@@ -674,6 +1067,14 @@ class PopulationGrowthTestCase(unittest.TestCase):
                     'gravity_immune': True,
                     'temperature_immune': True,
                     'radiation_immune': True,
+                    'population_per_r': 1000,
+                    'factories_produce_r': 10,
+                    'factories_cost_r': 10,
+                    'factories_per_pop': 10,
+                    'factories_cost_less': False,
+                    'minerals_per_m': 10,
+                    'mines_cost_r': 5,
+                    'mines_per_pop': 10,
                 },
                 {
                     'pk': 1,
diff --git a/tests/test_utils.py b/tests/test_utils.py
index df720b8..70b25fd 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -3,19 +3,57 @@ import unittest
 from universe import components, engine, utils
 
 
+class PlanetProductionTestCase(unittest.TestCase):
+    def test_values(self):
+        manager = engine.Manager()
+        manager.register_entity_type('species', [
+            components.SpeciesProductionComponent(),
+        ])
+        manager.register_entity_type('planet', [
+            components.PopulationComponent(),
+        ])
+        engine.Entity.register_manager(manager)
+
+        data = [
+            (700, 0, 0),
+            (700, 699, 0),
+            (700, 700, 1),
+            (700, 1_000_000, 1428),
+            (1000, 0, 0),
+            (1000, 999, 0),
+            (1000, 1000, 1),
+            (1000, 1_000_000, 1000),
+            (1500, 0, 0),
+            (1500, 1499, 0),
+            (1500, 1500, 1),
+            (1500, 1_000_000, 666),
+        ]
+
+        for per, pop, value in data:
+            species = engine.Entity(**{
+                'type': 'species',
+                'population_per_r': per,
+                'minerals_per_m': 5,
+                'mines_cost_r': 2,
+                'mines_per_pop': 5,
+            })
+            planet = engine.Entity(**{
+                'type': 'planet',
+                'population': pop,
+            })
+            self.assertEqual(utils.production(species, planet), value,
+                             f"population_per_r: {per}, population: {pop}")
+
+
 class PlanetValueTestCase(unittest.TestCase):
     def test_values(self):
         manager = engine.Manager()
-        manager._entity_registry = {
-            'species': {
-                'metadata': components.MetadataComponent(),
-                'species': components.SpeciesComponent(),
-            },
-            'planet': {
-                'metadata': components.MetadataComponent(),
-                'environment': components.EnvironmentComponent(),
-            },
-        }
+        manager.register_entity_type('species', [
+            components.SpeciesComponent(),
+        ])
+        manager.register_entity_type('planet', [
+            components.EnvironmentComponent(),
+        ])
         engine.Entity.register_manager(manager)
 
         species = engine.Entity(**