Repository: jbradberry/universe

Issue:
Add a way of providing a new unique id for freshly created entities
Currently we have been instantiating entities from serialized data, with ids already in place.  However, we will eventually need to have new objects come into the world (e.g. ship construction completes), so we need to keep track of and provide new unique ids for these new entities as needed.

Discussion:

Patch:
diff --git a/universe/components.py b/universe/components.py
index 34cf944..7d9a784 100644
--- a/universe/components.py
+++ b/universe/components.py
@@ -51,6 +51,7 @@ class Component(metaclass=MetaComponent):
 class MetadataComponent(Component):
     _name = 'metadata'
 
+    pk = fields.PrimaryKey()
     type = fields.CharField(required=True)
 
 
diff --git a/universe/engine.py b/universe/engine.py
index 1325317..1e0add8 100644
--- a/universe/engine.py
+++ b/universe/engine.py
@@ -61,6 +61,7 @@ class Entity:
 
 class Manager:
     def __init__(self):
+        self._seq = 0
         self._components = {}
         self._systems = []
         self._updates = {}
@@ -85,14 +86,17 @@ class Manager:
     def get_entity(self, _type, _id):
         return self._components.get(_type, {}).get(_id)
 
-    def set_entity(self, _type, _id, entity):
-        self._components.setdefault(_type, {})[_id] = entity
+    def set_entity(self, _type, entity):
+        self._components.setdefault(_type, {})[entity.pk] = entity
 
-    def register_entity(self, _id, entity):
-        entity_obj = Entity(**entity)
-        entity_obj.pk = _id
-        for component in entity_obj._components:
-            self.set_entity(component, _id, entity_obj)
+    def register_entity(self, entity):
+        if not isinstance(entity, Entity):
+            entity = Entity(**entity)
+        if entity.pk is None:
+            entity.pk = self._seq
+            self._seq += 1
+        for component in entity._components:
+            self.set_entity(component, entity)
 
     def process(self):
         for system_cls in self._systems:
@@ -100,16 +104,19 @@ class Manager:
             system.process(self)
 
     def import_data(self, data, updates):
-        for _id, entity in (data.get('entities') or {}).items():
-            self.register_entity(_id, entity)
+        if 'seq' in data:
+            self._seq = data['seq']
+        for entity in (data.get('entities') or ()):
+            self.register_entity(entity)
         for entity in self.get_entities('metadata').values():
             entity.validate()
         self._updates = updates
 
     def export_data(self):
-        data = {_id: entity for _id, entity in self.get_entities('metadata').items()}
-
-        return {'entities': {_id: entity.serialize() for _id, entity in data.items()}}
+        return {
+            'seq': self._seq,
+            'entities': [entity.serialize() for entity in self.get_entities('metadata').values()]
+        }
 
 
 class GameState:
diff --git a/universe/fields.py b/universe/fields.py
index 176aefd..3271091 100644
--- a/universe/fields.py
+++ b/universe/fields.py
@@ -75,6 +75,17 @@ class CharField(Field):
             raise exceptions.ValidationError(f"{self.data_name!r} must be a string.")
 
 
+class PrimaryKey(Field):
+    def __init__(self):
+        super().__init__()
+
+    def validate(self, data):
+        super().validate(data)
+        value = data[self.name]
+        if not isinstance(value, int):
+            raise exceptions.ValidationError(f"{self.name!r} must be an integer.")
+
+
 class Reference(Field):
     def __init__(self, types=None, **kwargs):
         super().__init__(**kwargs)


Test patch:
diff --git a/tests/test_components.py b/tests/test_components.py
index c845fe0..ddb7c94 100644
--- a/tests/test_components.py
+++ b/tests/test_components.py
@@ -50,8 +50,17 @@ class ComponentTestCase(unittest.TestCase):
 
 
 class MetadataComponentTestCase(unittest.TestCase):
+    def setUp(self):
+        self.manager = engine.Manager()
+        self.manager._entity_registry = {
+            'something': {
+                'metadata': components.MetadataComponent(),
+            },
+        }
+        engine.Entity.register_manager(self.manager)
+
     def test_simple(self):
-        data = {'type': 'something'}
+        data = {'type': 'something', 'pk': 0}
         component = components.MetadataComponent()
         self.assertEqual(component.serialize(data), data)
 
@@ -213,24 +222,24 @@ class OwnershipComponentTestCase(unittest.TestCase):
         engine.Entity.register_manager(self.manager)
 
     def test_null_pointer(self):
-        self.manager.register_entity(0, {'type': 'planet'})
+        self.manager.register_entity({'pk': 0, 'type': 'planet'})
 
         planet = self.manager.get_entity('metadata', 0)
         self.assertIsNone(planet.owner)
 
     def test_entity_reference(self):
-        self.manager.register_entity(0, {'type': 'species'})
-        self.manager.register_entity(1, {'type': 'planet', 'owner_id': 0})
+        self.manager.register_entity({'pk': 0, 'type': 'species'})
+        self.manager.register_entity({'pk': 1, 'type': 'planet', 'owner_id': 0})
 
         planet = self.manager.get_entity('metadata', 1)
 
         self.assertIsInstance(planet.owner, engine.Entity)
-        self.assertEqual(planet.owner.serialize(), {'type': 'species'})
+        self.assertEqual(planet.owner.serialize(), {'pk': 0, 'type': 'species'})
 
     def test_set_reference(self):
-        self.manager.register_entity(0, {'type': 'species'})
-        self.manager.register_entity(1, {'type': 'species'})
-        self.manager.register_entity(2, {'type': 'planet'})
+        self.manager.register_entity({'pk': 0, 'type': 'species'})
+        self.manager.register_entity({'pk': 1, 'type': 'species'})
+        self.manager.register_entity({'pk': 2, 'type': 'planet'})
 
         planet = self.manager.get_entity('metadata', 2)
         self.assertIsNone(planet.owner)
@@ -253,8 +262,8 @@ class OwnershipComponentTestCase(unittest.TestCase):
         self.assertIsNone(planet.owner_id)
 
     def test_wrong_owner_type(self):
-        self.manager.register_entity(0, {'type': 'planet'})
-        self.manager.register_entity(1, {'type': 'planet'})
+        self.manager.register_entity({'pk': 0, 'type': 'planet'})
+        self.manager.register_entity({'pk': 1, 'type': 'planet'})
 
         planet0 = self.manager.get_entity('metadata', 0)
         planet1 = self.manager.get_entity('metadata', 1)
diff --git a/tests/test_engine.py b/tests/test_engine.py
index c935bea..869f417 100644
--- a/tests/test_engine.py
+++ b/tests/test_engine.py
@@ -8,8 +8,9 @@ class EntityTestCase(unittest.TestCase):
         state = {
             'turn': 2500,
             'width': 1000,
-            'entities': {
-                1: {
+            'entities': [
+                {
+                    'pk': 1,
                     'type': 'planet',
                     'x': 300,
                     'y': 600,
@@ -26,7 +27,7 @@ class EntityTestCase(unittest.TestCase):
                     'owner_id': 0,  # Not an entity in the pool.
                     'population': 1000,
                 }
-            }
+            ]
         }
 
         S = engine.GameState(state, {})
@@ -41,8 +42,9 @@ class PersistenceTestCase(unittest.TestCase):
         state = {
             'turn': 2500,
             'width': 1000,
-            'entities': {
-                0: {
+            'entities': [
+                {
+                    'pk': 0,
                     'type': 'species',
                     'name': 'Human',
                     'plural_name': 'Humans',
@@ -51,7 +53,8 @@ class PersistenceTestCase(unittest.TestCase):
                     'temperature_immune': True,
                     'radiation_immune': True,
                 },
-                1: {
+                {
+                    'pk': 1,
                     'type': 'planet',
                     'x': 300,
                     'y': 600,
@@ -68,36 +71,38 @@ class PersistenceTestCase(unittest.TestCase):
                     'owner_id': 0,
                     'population': 1000,
                 }
-            }
+            ]
         }
 
         S = engine.GameState(state, {})
         results = S.generate()
 
         self.assertEqual(results['turn'], 2501)
-        self.assertIn(1, results['entities'])
-        self.assertEqual(results['entities'][1]['type'], 'planet')
-        self.assertEqual(results['entities'][1]['x'], 300)
-        self.assertEqual(results['entities'][1]['y'], 600)
-        self.assertEqual(results['entities'][1]['gravity'], 27)
-        self.assertEqual(results['entities'][1]['temperature'], 36)
-        self.assertEqual(results['entities'][1]['radiation'], 45)
-        self.assertEqual(results['entities'][1]['ironium_conc'], 67)
-        self.assertEqual(results['entities'][1]['boranium_conc'], 78)
-        self.assertEqual(results['entities'][1]['germanium_conc'], 82)
-        self.assertEqual(results['entities'][1]['ironium'], 20)
-        self.assertEqual(results['entities'][1]['boranium'], 30)
-        self.assertEqual(results['entities'][1]['germanium'], 40)
-        self.assertEqual(results['entities'][1]['queue'], [])
-        self.assertEqual(results['entities'][1]['owner_id'], 0)
-        self.assertGreater(results['entities'][1]['population'], 0)
+        self.assertEqual(len(results['entities']), 2)
+        planet = next(E for E in results['entities'] if E['pk'] == 1)
+        self.assertEqual(planet['type'], 'planet')
+        self.assertEqual(planet['x'], 300)
+        self.assertEqual(planet['y'], 600)
+        self.assertEqual(planet['gravity'], 27)
+        self.assertEqual(planet['temperature'], 36)
+        self.assertEqual(planet['radiation'], 45)
+        self.assertEqual(planet['ironium_conc'], 67)
+        self.assertEqual(planet['boranium_conc'], 78)
+        self.assertEqual(planet['germanium_conc'], 82)
+        self.assertEqual(planet['ironium'], 20)
+        self.assertEqual(planet['boranium'], 30)
+        self.assertEqual(planet['germanium'], 40)
+        self.assertEqual(planet['queue'], [])
+        self.assertEqual(planet['owner_id'], 0)
+        self.assertGreater(planet['population'], 0)
 
     def test_ship(self):
         state = {
             'turn': 2500,
             'width': 1000,
-            'entities': {
-                0: {
+            'entities': [
+                {
+                    'pk': 0,
                     'type': 'species',
                     'name': 'Human',
                     'plural_name': 'Humans',
@@ -106,7 +111,8 @@ class PersistenceTestCase(unittest.TestCase):
                     'temperature_immune': True,
                     'radiation_immune': True,
                 },
-                1: {
+                {
+                    'pk': 1,
                     'type': 'ship',
                     'x': 300,
                     'y': 600,
@@ -117,30 +123,32 @@ class PersistenceTestCase(unittest.TestCase):
                     'owner_id': 0,
                     'population': 1000,
                 }
-            }
+            ]
         }
 
         S = engine.GameState(state, {})
         results = S.generate()
 
         self.assertEqual(results['turn'], 2501)
-        self.assertIn(1, results['entities'])
-        self.assertEqual(results['entities'][1]['type'], 'ship')
-        self.assertEqual(results['entities'][1]['x'], 300)
-        self.assertEqual(results['entities'][1]['y'], 600)
-        self.assertEqual(results['entities'][1]['ironium'], 20)
-        self.assertEqual(results['entities'][1]['boranium'], 30)
-        self.assertEqual(results['entities'][1]['germanium'], 40)
-        self.assertEqual(results['entities'][1]['queue'], [])
-        self.assertEqual(results['entities'][1]['owner_id'], 0)
-        self.assertGreater(results['entities'][1]['population'], 0)
+        self.assertEqual(len(results['entities']), 2)
+        ship = next(E for E in results['entities'] if E['pk'] == 1)
+        self.assertEqual(ship['type'], 'ship')
+        self.assertEqual(ship['x'], 300)
+        self.assertEqual(ship['y'], 600)
+        self.assertEqual(ship['ironium'], 20)
+        self.assertEqual(ship['boranium'], 30)
+        self.assertEqual(ship['germanium'], 40)
+        self.assertEqual(ship['queue'], [])
+        self.assertEqual(ship['owner_id'], 0)
+        self.assertGreater(ship['population'], 0)
 
     def test_species(self):
         state = {
             'turn': 2500,
             'width': 1000,
-            'entities': {
-                0: {
+            'entities': [
+                {
+                    'pk': 0,
                     'type': 'species',
                     'name': 'Human',
                     'plural_name': 'Humans',
@@ -149,37 +157,38 @@ class PersistenceTestCase(unittest.TestCase):
                     'temperature_immune': True,
                     'radiation_immune': True,
                 },
-            }
+            ]
         }
 
         S = engine.GameState(state, {})
         results = S.generate()
 
         self.assertEqual(results['turn'], 2501)
-        self.assertIn(0, results['entities'])
-        self.assertEqual(results['entities'][0]['type'], 'species')
-        self.assertEqual(results['entities'][0]['name'], 'Human')
-        self.assertEqual(results['entities'][0]['plural_name'], 'Humans')
-        self.assertEqual(results['entities'][0]['growth_rate'], 15)
-        self.assertEqual(results['entities'][0]['gravity_immune'], True)
-        self.assertEqual(results['entities'][0]['temperature_immune'], True)
-        self.assertEqual(results['entities'][0]['radiation_immune'], True)
+        self.assertEqual(len(results['entities']), 1)
+        species = next(E for E in results['entities'] if E['pk'] == 0)
+        self.assertEqual(species['type'], 'species')
+        self.assertEqual(species['name'], 'Human')
+        self.assertEqual(species['plural_name'], 'Humans')
+        self.assertEqual(species['growth_rate'], 15)
+        self.assertEqual(species['gravity_immune'], True)
+        self.assertEqual(species['temperature_immune'], True)
+        self.assertEqual(species['radiation_immune'], True)
 
 
 class MovementTestCase(unittest.TestCase):
     def test_empty_universe(self):
-        state = {'turn': 2500, 'width': 1000, 'entities': {}}
+        state = {'turn': 2500, 'width': 1000, 'entities': []}
         updates = {}
 
         S = engine.GameState(state, updates)
         results = S.generate()
 
         self.assertEqual(results,
-                         {'turn': 2501, 'width': 1000, 'entities': {}})
+                         {'turn': 2501, 'width': 1000, 'seq': 0, 'entities': []})
 
     def test_one_stationary_object(self):
         state = {'turn': 2500, 'width': 1000,
-                 'entities': {0: {'type': 'ship', 'x': 456, 'y': 337, 'queue': []}}}
+                 'entities': [{'pk': 0, 'type': 'ship', 'x': 456, 'y': 337, 'queue': []}]}
         updates = {}
 
         S = engine.GameState(state, updates)
@@ -189,12 +198,12 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 456, 'x_prev': 456, 'y': 337, 'y_prev': 337, 'queue': []}}
+            [{'pk': 0, 'type': 'ship', 'x': 456, 'x_prev': 456, 'y': 337, 'y_prev': 337, 'queue': []}]
         )
 
     def test_one_object_single_turn_move(self):
         state = {'turn': 2500, 'width': 1000,
-                 'entities': {0: {'type': 'ship', 'x': 480, 'y': 235, 'queue': []}}}
+                 'entities': [{'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': []}]}
         # actual speed is warp**2
         updates = {0: [{'seq': 0, 'x_t': 422, 'y_t': 210, 'warp': 10}]}
 
@@ -205,12 +214,12 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 422, 'x_prev': 480, 'y': 210, 'y_prev': 235, 'queue': []}}
+            [{'pk': 0, 'type': 'ship', 'x': 422, 'x_prev': 480, 'y': 210, 'y_prev': 235, 'queue': []}]
         )
 
     def test_multi_turn_single_move(self):
         state = {'turn': 2500, 'width': 1000,
-                 'entities': {0: {'type': 'ship', 'x': 480, 'y': 235, 'queue': []}}}
+                 'entities': [{'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': []}]}
         # actual speed is warp**2
         updates = {0: [{'seq': 0, 'x_t': 168, 'y_t': 870, 'warp': 10}]}
 
@@ -221,14 +230,17 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 436, 'x_prev': 480, 'y': 325, 'y_prev': 235,
-                 'queue': [{'x_t': 168, 'y_t': 870, 'warp': 10}]}}
+            [{'pk': 0, 'type': 'ship', 'x': 436, 'x_prev': 480, 'y': 325, 'y_prev': 235,
+              'queue': [{'x_t': 168, 'y_t': 870, 'warp': 10}]}]
         )
 
     def test_previously_queued_move(self):
-        state = {'turn': 2500, 'width': 1000,
-                 'entities': {
-                     0: {'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'x_t': 422, 'y_t': 210, 'warp': 10}]}}}
+        state = {
+            'turn': 2500, 'width': 1000,
+            'entities': [
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'x_t': 422, 'y_t': 210, 'warp': 10}]}
+            ]
+        }
         updates = {}
 
         S = engine.GameState(state, updates)
@@ -238,13 +250,16 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 422, 'x_prev': 480, 'y': 210, 'y_prev': 235, 'queue': []}}
+            [{'pk': 0, 'type': 'ship', 'x': 422, 'x_prev': 480, 'y': 210, 'y_prev': 235, 'queue': []}]
         )
 
     def test_replace_queued_move(self):
-        state = {'turn': 2500, 'width': 1000,
-                 'entities': {
-                     0: {'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'x_t': 637, 'y_t': 786, 'warp': 8}]}}}
+        state = {
+            'turn': 2500, 'width': 1000,
+            'entities': [
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'x_t': 637, 'y_t': 786, 'warp': 8}]}
+            ]
+        }
         updates = {0: [{'seq': 0, 'x_t': 422, 'y_t': 210, 'warp': 10}]}
 
         S = engine.GameState(state, updates)
@@ -254,13 +269,16 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 422, 'x_prev': 480, 'y': 210, 'y_prev': 235, 'queue': []}}
+            [{'pk': 0, 'type': 'ship', 'x': 422, 'x_prev': 480, 'y': 210, 'y_prev': 235, 'queue': []}]
         )
 
     def test_add_to_queued_moves(self):
-        state = {'turn': 2500, 'width': 1000,
-                 'entities': {
-                     0: {'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'x_t': 422, 'y_t': 210, 'warp': 10}]}}}
+        state = {
+            'turn': 2500, 'width': 1000,
+            'entities': [
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'x_t': 422, 'y_t': 210, 'warp': 10}]}
+            ]
+        }
         updates = {0: [{'seq': 1, 'x_t': 637, 'y_t': 786, 'warp': 8}]}
 
         S = engine.GameState(state, updates)
@@ -270,14 +288,16 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 422, 'x_prev': 480, 'y': 210, 'y_prev': 235,
-                 'queue': [{'x_t': 637, 'y_t': 786, 'warp': 8}]}}
+            [{'pk': 0, 'type': 'ship', 'x': 422, 'x_prev': 480, 'y': 210, 'y_prev': 235,
+              'queue': [{'x_t': 637, 'y_t': 786, 'warp': 8}]}]
         )
 
     def test_slow_motion(self):
         state = {
             'turn': 2500, 'width': 1000,
-            'entities': {0: {'type': 'ship', 'x': 500, 'y': 500, 'queue': [{'x_t': 501, 'y_t': 500, 'warp': 1}]}}
+            'entities': [
+                {'pk': 0, 'type': 'ship', 'x': 500, 'y': 500, 'queue': [{'x_t': 501, 'y_t': 500, 'warp': 1}]}
+            ]
         }
 
         S = engine.GameState(state, {})
@@ -287,13 +307,17 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 501, 'x_prev': 500, 'y': 500, 'y_prev': 500, 'queue': []}}
+            [{'pk': 0, 'type': 'ship', 'x': 501, 'x_prev': 500, 'y': 500, 'y_prev': 500, 'queue': []}]
         )
 
     def test_target_stationary_object(self):
-        state = {'turn': 2500, 'width': 1000,
-                 'entities': {0: {'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'target_id': 1, 'warp': 10}]},
-                              1: {'type': 'ship', 'x': 460, 'y': 215, 'queue': []}}}
+        state = {
+            'turn': 2500, 'width': 1000,
+            'entities': [
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'target_id': 1, 'warp': 10}]},
+                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215, 'queue': []}
+            ]
+        }
         updates = {}
 
         S = engine.GameState(state, updates)
@@ -303,15 +327,17 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 460, 'x_prev': 480, 'y': 215, 'y_prev': 235, 'queue': []},
-             1: {'type': 'ship', 'x': 460, 'x_prev': 460, 'y': 215, 'y_prev': 215, 'queue': []}}
+            [{'pk': 0, 'type': 'ship', 'x': 460, 'x_prev': 480, 'y': 215, 'y_prev': 235, 'queue': []},
+             {'pk': 1, 'type': 'ship', 'x': 460, 'x_prev': 460, 'y': 215, 'y_prev': 215, 'queue': []}]
         )
 
     def test_target_moving_object(self):
         state = {
             'turn': 2500, 'width': 1000,
-            'entities': {0: {'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'target_id': 1, 'warp': 10}]},
-                         1: {'type': 'ship', 'x': 460, 'y': 215, 'queue': [{'x_t': 465, 'y_t': 220, 'warp': 10}]}}
+            'entities': [
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'target_id': 1, 'warp': 10}]},
+                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215, 'queue': [{'x_t': 465, 'y_t': 220, 'warp': 10}]}
+            ]
         }
         updates = {}
 
@@ -322,15 +348,17 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 465, 'x_prev': 480, 'y': 220, 'y_prev': 235, 'queue': []},
-             1: {'type': 'ship', 'x': 465, 'x_prev': 460, 'y': 220, 'y_prev': 215, 'queue': []}}
+            [{'pk': 0, 'type': 'ship', 'x': 465, 'x_prev': 480, 'y': 220, 'y_prev': 235, 'queue': []},
+             {'pk': 1, 'type': 'ship', 'x': 465, 'x_prev': 460, 'y': 220, 'y_prev': 215, 'queue': []}]
         )
 
     def test_target_can_barely_reach(self):
         state = {
             'turn': 2500, 'width': 1000,
-            'entities': {0: {'type': 'ship', 'x': 560, 'y': 315, 'queue': [{'target_id': 1, 'warp': 10}]},
-                         1: {'type': 'ship', 'x': 460, 'y': 215, 'queue': [{'x_t': 660, 'y_t': 215, 'warp': 10}]}}
+            'entities': [
+                {'pk': 0, 'type': 'ship', 'x': 560, 'y': 315, 'queue': [{'target_id': 1, 'warp': 10}]},
+                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215, 'queue': [{'x_t': 660, 'y_t': 215, 'warp': 10}]}
+            ]
         }
         S = engine.GameState(state, {})
         results = S.generate()
@@ -339,16 +367,18 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 560, 'x_prev': 560, 'y': 215, 'y_prev': 315, 'queue': []},
-             1: {'type': 'ship', 'x': 560, 'x_prev': 460, 'y': 215, 'y_prev': 215,
-                 'queue': [{'x_t': 660, 'y_t': 215, 'warp': 10}]}}
+            [{'pk': 0, 'type': 'ship', 'x': 560, 'x_prev': 560, 'y': 215, 'y_prev': 315, 'queue': []},
+             {'pk': 1, 'type': 'ship', 'x': 560, 'x_prev': 460, 'y': 215, 'y_prev': 215,
+              'queue': [{'x_t': 660, 'y_t': 215, 'warp': 10}]}]
         )
 
     def test_target_stops_short(self):
         state = {
             'turn': 2500, 'width': 1000,
-            'entities': {0: {'type': 'ship', 'x': 560, 'y': 315, 'queue': [{'target_id': 1, 'warp': 10}]},
-                         1: {'type': 'ship', 'x': 460, 'y': 215, 'queue': [{'x_t': 510, 'y_t': 215, 'warp': 10}]}}
+            'entities': [
+                {'pk': 0, 'type': 'ship', 'x': 560, 'y': 315, 'queue': [{'target_id': 1, 'warp': 10}]},
+                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215, 'queue': [{'x_t': 510, 'y_t': 215, 'warp': 10}]}
+            ]
         }
         S = engine.GameState(state, {})
         results = S.generate()
@@ -359,15 +389,19 @@ class MovementTestCase(unittest.TestCase):
         # i.e. roughly 35 lightyears in each direction
         self.assertEqual(
             results['entities'],
-            {0: {'type': 'ship', 'x': 524, 'x_prev': 560, 'y': 230, 'y_prev': 315,
-                 'queue': [{'target_id': 1, 'warp': 10}]},
-             1: {'type': 'ship', 'x': 510, 'x_prev': 460, 'y': 215, 'y_prev': 215, 'queue': []}}
+            [{'pk': 0, 'type': 'ship', 'x': 524, 'x_prev': 560, 'y': 230, 'y_prev': 315,
+              'queue': [{'target_id': 1, 'warp': 10}]},
+             {'pk': 1, 'type': 'ship', 'x': 510, 'x_prev': 460, 'y': 215, 'y_prev': 215, 'queue': []}]
         )
 
     def test_mutual_intercept(self):
-        state = {'turn': 2500, 'width': 1000,
-                 'entities': {0: {'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'target_id': 1, 'warp': 10}]},
-                              1: {'type': 'ship', 'x': 460, 'y': 215, 'queue': [{'target_id': 0, 'warp': 10}]}}}
+        state = {
+            'turn': 2500, 'width': 1000,
+            'entities': [
+                {'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'target_id': 1, 'warp': 10}]},
+                {'type': 'ship', 'x': 460, 'y': 215, 'queue': [{'target_id': 0, 'warp': 10}]}
+            ]
+        }
         updates = {}
 
         S = engine.GameState(state, updates)
@@ -376,16 +410,20 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['turn'], 2501)
         self.assertEqual(results['width'], 1000)
         self.assertEqual(len(results['entities']), 2)
-        coordinates = {(entity['x'], entity['y']) for entity in results['entities'].values()}
+        coordinates = {(entity['x'], entity['y']) for entity in results['entities']}
         self.assertEqual(coordinates, {(470, 225)})
         self.assertEqual(results['entities'][0]['queue'], [])
         self.assertEqual(results['entities'][1]['queue'], [])
 
     def test_three_way_cycle_intercept(self):
-        state = {'turn': 2500, 'width': 1000,
-                 'entities': {0: {'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'target_id': 1, 'warp': 10}]},
-                              1: {'type': 'ship', 'x': 460, 'y': 215, 'queue': [{'target_id': 2, 'warp': 10}]},
-                              2: {'type': 'ship', 'x': 500, 'y': 205, 'queue': [{'target_id': 0, 'warp': 10}]}}}
+        state = {
+            'turn': 2500, 'width': 1000,
+            'entities': [
+                {'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'target_id': 1, 'warp': 10}]},
+                {'type': 'ship', 'x': 460, 'y': 215, 'queue': [{'target_id': 2, 'warp': 10}]},
+                {'type': 'ship', 'x': 500, 'y': 205, 'queue': [{'target_id': 0, 'warp': 10}]}
+            ]
+        }
         updates = {}
 
         S = engine.GameState(state, updates)
@@ -394,16 +432,20 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['turn'], 2501)
         self.assertEqual(results['width'], 1000)
         self.assertEqual(len(results['entities']), 3)
-        coordinates = {(entity['x'], entity['y']) for entity in results['entities'].values()}
+        coordinates = {(entity['x'], entity['y']) for entity in results['entities']}
         self.assertEqual(coordinates, {(480, 217)})
-        self.assertTrue(all(not entity['queue'] for entity in results['entities'].values()))
+        self.assertTrue(all(not entity['queue'] for entity in results['entities']))
 
     def test_four_way_cycle_intercept(self):
-        state = {'turn': 2500, 'width': 1000,
-                 'entities': {0: {'type': 'ship', 'x': 500, 'y': 500, 'queue': [{'target_id': 1, 'warp': 10}]},
-                              1: {'type': 'ship', 'x': 600, 'y': 500, 'queue': [{'target_id': 2, 'warp': 10}]},
-                              2: {'type': 'ship', 'x': 600, 'y': 600, 'queue': [{'target_id': 3, 'warp': 10}]},
-                              3: {'type': 'ship', 'x': 500, 'y': 600, 'queue': [{'target_id': 0, 'warp': 10}]}}}
+        state = {
+            'turn': 2500, 'width': 1000,
+            'entities': [
+                {'pk': 0, 'type': 'ship', 'x': 500, 'y': 500, 'queue': [{'target_id': 1, 'warp': 10}]},
+                {'pk': 1, 'type': 'ship', 'x': 600, 'y': 500, 'queue': [{'target_id': 2, 'warp': 10}]},
+                {'pk': 2, 'type': 'ship', 'x': 600, 'y': 600, 'queue': [{'target_id': 3, 'warp': 10}]},
+                {'pk': 3, 'type': 'ship', 'x': 500, 'y': 600, 'queue': [{'target_id': 0, 'warp': 10}]}
+            ]
+        }
         updates = {}
 
         S = engine.GameState(state, updates)
@@ -412,6 +454,6 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['turn'], 2501)
         self.assertEqual(results['width'], 1000)
         self.assertEqual(len(results['entities']), 4)
-        coordinates = {(entity['x'], entity['y']) for entity in results['entities'].values()}
+        coordinates = {(entity['x'], entity['y']) for entity in results['entities']}
         self.assertEqual(coordinates, {(550, 550)})
-        self.assertTrue(all(not entity['queue'] for entity in results['entities'].values()))
+        self.assertTrue(all(not entity['queue'] for entity in results['entities']))
diff --git a/tests/test_systems.py b/tests/test_systems.py
index 22a774a..c5df266 100644
--- a/tests/test_systems.py
+++ b/tests/test_systems.py
@@ -9,8 +9,9 @@ class PopulationTestCase(unittest.TestCase):
         state = {
             'turn': 2500,
             'width': 1000,
-            'entities': {
-                0: {
+            'entities': [
+                {
+                    'pk': 0,
                     'type': 'species',
                     'name': 'Human',
                     'plural_name': 'Humans',
@@ -19,7 +20,8 @@ class PopulationTestCase(unittest.TestCase):
                     'temperature_immune': True,
                     'radiation_immune': True,
                 },
-                1: {
+                {
+                    'pk': 1,
                     'type': 'planet',
                     'population': 1000,
                     'x': 480,
@@ -33,7 +35,7 @@ class PopulationTestCase(unittest.TestCase):
                     'owner_id': 0,
                     'queue': [],
                 },
-            }
+            ]
         }
 
         S = engine.GameState(state, {})
@@ -47,8 +49,9 @@ class PopulationTestCase(unittest.TestCase):
         state = {
             'turn': 2500,
             'width': 1000,
-            'entities': {
-                0: {
+            'entities': [
+                {
+                    'pk': 0,
                     'type': 'species',
                     'name': 'Human',
                     'plural_name': 'Humans',
@@ -57,7 +60,8 @@ class PopulationTestCase(unittest.TestCase):
                     'temperature_immune': True,
                     'radiation_immune': True,
                 },
-                1: {
+                {
+                    'pk': 1,
                     'type': 'planet',
                     'population': 500_000,
                     'x': 480,
@@ -71,7 +75,7 @@ class PopulationTestCase(unittest.TestCase):
                     'owner_id': 0,
                     'queue': [],
                 },
-            }
+            ]
         }
 
         S = engine.GameState(state, {})
@@ -88,8 +92,9 @@ class PopulationTestCase(unittest.TestCase):
         state = {
             'turn': 2500,
             'width': 1000,
-            'entities': {
-                0: {
+            'entities': [
+                {
+                    'pk': 0,
                     'type': 'species',
                     'name': 'Human',
                     'plural_name': 'Humans',
@@ -104,7 +109,8 @@ class PopulationTestCase(unittest.TestCase):
                     'radiation_min': 38,
                     'radiation_max': 90,
                 },
-                1: {
+                {
+                    'pk': 1,
                     'type': 'planet',
                     'population': 1_000_000,
                     'x': 480,
@@ -118,7 +124,7 @@ class PopulationTestCase(unittest.TestCase):
                     'owner_id': 0,
                     'queue': [],
                 },
-            }
+            ]
         }
 
         S = engine.GameState(state, {})
@@ -132,8 +138,9 @@ class PopulationTestCase(unittest.TestCase):
         state = {
             'turn': 2500,
             'width': 1000,
-            'entities': {
-                0: {
+            'entities': [
+                {
+                    'pk': 0,
                     'type': 'species',
                     'name': 'Human',
                     'plural_name': 'Humans',
@@ -148,7 +155,8 @@ class PopulationTestCase(unittest.TestCase):
                     'radiation_min': 38,
                     'radiation_max': 90,
                 },
-                1: {
+                {
+                    'pk': 1,
                     'type': 'planet',
                     'population': 2_000_000,
                     'x': 480,
@@ -162,7 +170,7 @@ class PopulationTestCase(unittest.TestCase):
                     'owner_id': 0,
                     'queue': [],
                 },
-            }
+            ]
         }
 
         S = engine.GameState(state, {})
@@ -178,8 +186,9 @@ class PopulationTestCase(unittest.TestCase):
         state = {
             'turn': 2500,
             'width': 1000,
-            'entities': {
-                0: {
+            'entities': [
+                {
+                    'pk': 0,
                     'type': 'species',
                     'name': 'Human',
                     'plural_name': 'Humans',
@@ -194,7 +203,8 @@ class PopulationTestCase(unittest.TestCase):
                     'radiation_min': 38,
                     'radiation_max': 90,
                 },
-                1: {
+                {
+                    'pk': 1,
                     'type': 'planet',
                     'population': 100_000,
                     'x': 480,
@@ -208,7 +218,7 @@ class PopulationTestCase(unittest.TestCase):
                     'owner_id': 0,
                     'queue': [],
                 },
-            }
+            ]
         }
 
         S = engine.GameState(state, {})
@@ -224,8 +234,9 @@ class PopulationTestCase(unittest.TestCase):
         state = {
             'turn': 2500,
             'width': 1000,
-            'entities': {
-                0: {
+            'entities': [
+                {
+                    'pk': 0,
                     'type': 'species',
                     'name': 'Human',
                     'plural_name': 'Humans',
@@ -234,7 +245,8 @@ class PopulationTestCase(unittest.TestCase):
                     'temperature_immune': True,
                     'radiation_immune': True,
                 },
-                1: {
+                {
+                    'pk': 1,
                     'type': 'planet',
                     'population': 0,
                     'x': 480,
@@ -248,7 +260,7 @@ class PopulationTestCase(unittest.TestCase):
                     'owner_id': 0,
                     'queue': [],
                 },
-            }
+            ]
         }
 
         S = engine.GameState(state, {})