Repository: jbradberry/universe

Issue:
Add planetary environmental variables
Planets need to have environmental settings which will influence their habitability for the players' species.

- [x] create a new environment component, consisting of temperature, radiation, and gravity
- [x] add the new component to the planet entity
- [x] have a mapping or formula from the environmental variable value to display value (e.g. bin number 15 for gravity might show up as "0.75g").  Derive these from the values they have in Stars, for now.
- [x] write a function or functions that generate random values of these settings, for the generation of an initial map.  These functions do not yet have to be hooked up to anything.  Refer to Stars to find out the random distribution of these variables (not all of them are uniformly distributed)

Discussion:

Patch:
diff --git a/universe/components.py b/universe/components.py
index 7bba5cb..85c9ed2 100644
--- a/universe/components.py
+++ b/universe/components.py
@@ -1,3 +1,6 @@
+import math
+import random
+
 from . import fields
 
 
@@ -30,6 +33,20 @@ class Component(metaclass=MetaComponent):
                 raise ValidationError(f"{name} is required.")
         return output
 
+    def display(self, data):
+        output = {}
+        for name, field in self._fields.items():
+            if name in data:
+                value = data[name]
+                if hasattr(self, f'_display_{name}'):
+                    value = getattr(self, f'_display_{name}')(value)
+                else:
+                    value = str(value)
+                output[name] = value
+            elif getattr(field, 'required', True):
+                raise ValidationError(f"{name} is required.")
+        return output
+
 
 class MetadataComponent(Component):
     _name = 'metadata'
@@ -74,3 +91,33 @@ class PopulationComponent(Component):
     _name = 'population'
 
     population = fields.IntField(required=False)
+
+
+class EnvironmentComponent(Component):
+    _name = 'environment'
+
+    gravity = fields.IntField(min=0, max=100)
+    temperature = fields.IntField(min=0, max=100)
+    radiation = fields.IntField(min=0, max=100)
+
+    def _display_gravity(self, value):
+        gravity = math.pow(2, 6. * value / 100 - 3)
+        return f"{gravity:0.3f}g"
+
+    def _display_temperature(self, value):
+        temp = 4 * value - 200
+        return f"{temp}\u00b0C"
+
+    def _display_radiation(self, value):
+        return f"{value}mR"
+
+    @classmethod
+    def random(cls):
+        # Random weights taken from http://stars.arglos.net/articles/hm-charts.html
+
+        weights = list(range(1, 10)) + [10] * 81 + list(reversed(range(1, 10)))
+        return {
+            'gravity': random.choices(list(range(1, 100)), weights=weights)[0],
+            'temperature': random.choices(list(range(1, 100)), weights=weights)[0],
+            'radiation': random.randint(1, 99),
+        }
diff --git a/universe/engine.py b/universe/engine.py
index 1206f24..cd93175 100644
--- a/universe/engine.py
+++ b/universe/engine.py
@@ -107,6 +107,7 @@ class GameState:
         ])
         self.manager.register_entity_type('planet', [
             components.PositionComponent(),
+            components.EnvironmentComponent(),
             components.QueueComponent([
                 BuildInstallation, Terraform, BuildStation, BuildShip, LaunchMassPacket,
             ]),
diff --git a/universe/fields.py b/universe/fields.py
index 4a45bf4..d058137 100644
--- a/universe/fields.py
+++ b/universe/fields.py
@@ -5,7 +5,9 @@ class Field:
 
 
 class IntField(Field):
-    def __init__(self, required=True):
+    def __init__(self, min=None, max=None, required=True):
+        self.min = min
+        self.max = max
         self.required = required
 
 


Test patch:
diff --git a/tests/test_components.py b/tests/test_components.py
index 4df9612..eb03b9e 100644
--- a/tests/test_components.py
+++ b/tests/test_components.py
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 import unittest
 
 from universe import components
@@ -14,3 +15,37 @@ class MetadataComponentTestCase(unittest.TestCase):
         component = components.MetadataComponent()
         with self.assertRaises(components.ValidationError):
             component.serialize(data)
+
+
+class EnvironmentComponentTestCase(unittest.TestCase):
+    def test_display_fields(self):
+        data = {
+            'gravity': 35,
+            'temperature': 62,
+            'radiation': 50,
+        }
+        component = components.EnvironmentComponent()
+        self.assertEqual(component._display_gravity(data['gravity']), "0.536g")
+        self.assertEqual(component._display_temperature(data['temperature']), "48°C")
+        self.assertEqual(component._display_radiation(data['radiation']), "50mR")
+
+    def test_component_display(self):
+        data = {
+            'gravity': 35,
+            'temperature': 62,
+            'radiation': 50,
+        }
+        component = components.EnvironmentComponent()
+        self.assertEqual(
+            component.display(data),
+            {'gravity': "0.536g", 'temperature': "48°C", 'radiation': "50mR"}
+        )
+
+    def test_random(self):
+        data = components.EnvironmentComponent.random()
+        self.assertGreaterEqual(data['gravity'], 1)
+        self.assertLessEqual(data['gravity'], 99)
+        self.assertGreaterEqual(data['temperature'], 1)
+        self.assertLessEqual(data['temperature'], 99)
+        self.assertGreaterEqual(data['radiation'], 1)
+        self.assertLessEqual(data['radiation'], 99)
diff --git a/tests/test_systems.py b/tests/test_systems.py
index 6711970..2877a9a 100644
--- a/tests/test_systems.py
+++ b/tests/test_systems.py
@@ -21,6 +21,9 @@ class PopulationTestCase(unittest.TestCase):
                     'population': 1000,
                     'x': 480,
                     'y': 235,
+                    'gravity': 50,
+                    'temperature': 50,
+                    'radiation': 50,
                     'owner_id': 0,
                     'queue': [],
                 },
@@ -47,6 +50,9 @@ class PopulationTestCase(unittest.TestCase):
                                   'x_prev': 480,
                                   'y': 235,
                                   'y_prev': 235,
+                                  'gravity': 50,
+                                  'temperature': 50,
+                                  'radiation': 50,
                                   'owner_id': 0,
                                   'queue': [],
                               },