Repository: jbradberry/universe

Issue:
Orders in queues have particular types
and if a particular entity type's queue doesn't support an order type, that order cannot be issued.

- [x] validate queued orders based on the order types registered with the queue component
- [x] orders maybe should be entities
- [x] movement processing should be updated to only grab movement orders

This might all need to be reversed, if we make orders into a type of entity they should have a reference field pointing to a planet or a ship and then we can do away with a queue as a `ListField`.  We'll still need some kind of reverse reference collection, though, to make it easy to work with.

Discussion:

Patch:
diff --git a/universe/components.py b/universe/components.py
index 7d9a784..c9dbf3a 100644
--- a/universe/components.py
+++ b/universe/components.py
@@ -65,15 +65,6 @@ class PositionComponent(Component):
     y_prev = fields.IntField(required=False)
 
 
-class QueueComponent(Component):
-    _name = 'queue'
-
-    queue = fields.ListField()
-
-    def __init__(self, order_types):
-        self._order_types = order_types
-
-
 class SpeciesComponent(Component):
     _name = 'species'
 
@@ -188,3 +179,33 @@ class MineralInventoryComponent(Component):
     ironium = fields.IntField(min=0, required=False)
     boranium = fields.IntField(min=0, required=False)
     germanium = fields.IntField(min=0, required=False)
+
+
+class OrderComponent(Component):
+    _name = 'orders'
+
+    actor = fields.Reference(types=['planet', 'ship'])
+    seq = fields.IntField(min=0)
+
+
+class MovementComponent(Component):
+    _name = 'movement_orders'
+
+    warp = fields.IntField(min=0)
+    x_t = fields.IntField(required=False)
+    y_t = fields.IntField(required=False)
+    target = fields.Reference(types=['planet', 'ship'], required=False)
+
+    def validate(self, data):
+        super().validate(data)
+
+        from .engine import Entity
+        actor = Entity.manager.get_entity('metadata', data['actor_id'])
+        if actor.type != 'ship':
+            raise exceptions.ValidationError("The acting object must be a ship.")
+
+        if data['actor_id'] == data.get('target_id'):
+            raise exceptions.ValidationError("A ship cannot target itself for a movement order.")
+
+        if (data.get('target_id') is None) == (data.get('x_t') is None or data.get('y_t') is None):
+            raise exceptions.ValidationError("Either of 'target_id' or the target coordinates must be set.")
diff --git a/universe/engine.py b/universe/engine.py
index 1e0add8..0737feb 100644
--- a/universe/engine.py
+++ b/universe/engine.py
@@ -1,8 +1,6 @@
 import weakref
 
 from . import components, systems, exceptions
-from .orders import (Move, CargoTransfer, Scrap, BuildInstallation, Terraform,
-                     BuildStation, BuildShip, LaunchMassPacket)
 
 
 class Entity:
@@ -64,7 +62,7 @@ class Manager:
         self._seq = 0
         self._components = {}
         self._systems = []
-        self._updates = {}
+        self._updates = []
 
         self._entity_registry = {}
 
@@ -77,9 +75,6 @@ class Manager:
         _components.append(components.MetadataComponent())
         self._entity_registry[name] = {component._name: component for component in _components}
 
-    def get_updates(self, _id):
-        return self._updates.get(_id, [])
-
     def get_entities(self, _type):
         return self._components.get(_type, {})
 
@@ -89,6 +84,9 @@ class Manager:
     def set_entity(self, _type, entity):
         self._components.setdefault(_type, {})[entity.pk] = entity
 
+    def del_entity(self, _type, entity):
+        self._components.setdefault(_type, {}).pop(entity.pk, None)
+
     def register_entity(self, entity):
         if not isinstance(entity, Entity):
             entity = Entity(**entity)
@@ -98,6 +96,13 @@ class Manager:
         for component in entity._components:
             self.set_entity(component, entity)
 
+        return entity
+
+    def unregister_entity(self, entity):
+        for component in entity._components:
+            self.del_entity(component, entity)
+        entity.pk = None
+
     def process(self):
         for system_cls in self._systems:
             system = system_cls()
@@ -131,7 +136,6 @@ class GameState:
 
         self.manager.register_entity_type('ship', [
             components.PositionComponent(),
-            components.QueueComponent([Move, CargoTransfer, Scrap,]),
             components.OwnershipComponent(),
             components.PopulationComponent(),
             components.MineralInventoryComponent(),
@@ -141,15 +145,16 @@ class GameState:
             components.EnvironmentComponent(),
             components.MineralConcentrationComponent(),
             components.MineralInventoryComponent(),
-            components.QueueComponent([
-                BuildInstallation, Terraform, BuildStation, BuildShip, LaunchMassPacket,
-            ]),
             components.OwnershipComponent(),
             components.PopulationComponent(),
         ])
         self.manager.register_entity_type('species', [
             components.SpeciesComponent(),
         ])
+        self.manager.register_entity_type('movement_order', [
+            components.OrderComponent(),
+            components.MovementComponent(),
+        ])
         Entity.register_manager(self.manager)
 
         self.new = {}
diff --git a/universe/fields.py b/universe/fields.py
index 3271091..c6efd15 100644
--- a/universe/fields.py
+++ b/universe/fields.py
@@ -55,16 +55,6 @@ class IntField(Field):
             raise exceptions.ValidationError(f"{self.data_name!r} must be less than or equal to {self.max}.")
 
 
-class ListField(Field):
-    def validate(self, data):
-        super().validate(data)
-        if self.data_name not in data:
-            return
-        value = data[self.data_name]
-        if not isinstance(value, list):
-            raise exceptions.ValidationError(f"{self.data_name!r} must be a list.")
-
-
 class CharField(Field):
     def validate(self, data):
         super().validate(data)
diff --git a/universe/orders.py b/universe/orders.py
deleted file mode 100644
index 22bd2a0..0000000
--- a/universe/orders.py
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-class Order:
-    pass
-
-
-class Move(Order):
-    pass
-
-
-class CargoTransfer(Order):
-    pass
-
-
-class Scrap(Order):
-    pass
-
-
-class BuildInstallation(Order):
-    pass
-
-
-class Terraform(Order):
-    pass
-
-
-class BuildStation(Order):
-    pass
-
-
-class BuildShip(Order):
-    pass
-
-
-class LaunchMassPacket(Order):
-    pass
diff --git a/universe/systems.py b/universe/systems.py
index 5fcfa50..dba9577 100644
--- a/universe/systems.py
+++ b/universe/systems.py
@@ -1,3 +1,4 @@
+from collections import defaultdict
 from decimal import Decimal
 
 from . import utils
@@ -5,108 +6,110 @@ from . import utils
 
 class UpdateSystem:
     def process(self, manager):
-        for _id, entity in manager.get_entities('queue').items():
-            queue = entity.queue
-            indexed_queue = dict(enumerate(queue))
+        queues = defaultdict(dict)
+        for order in manager.get_entities('orders').values():
+            queues[order.actor_id][order.seq] = order
 
-            for action in manager.get_updates(_id):
-                seq = action.pop('seq')
-                indexed_queue[seq] = action
-
-            entity.queue = [action for seq, action in sorted(indexed_queue.items())]
+        for action in manager._updates:
+            order = manager.register_entity(action)
+            if order.seq in queues[order.actor_id]:
+                manager.unregister_entity(queues[order.actor_id][order.seq])
+            queues[order.actor_id][order.seq] = order
 
 
 class MovementSystem:
     N = 1000
 
-    def _vector_to_target(self, entity, manager):
-        move = entity.queue[0]
-        speed = Decimal(move['warp'] ** 2) / self.N
+    def _vector_to_target(self, move):
+        speed = Decimal(move.warp ** 2) / self.N
 
-        if 'target_id' in move:
-            target_entity = manager.get_entity('position', move['target_id'])
-            x_t, y_t = target_entity.x, target_entity.y
+        if move.target is not None:
+            x_t, y_t = move.target.x, move.target.y
         else:
-            x_t, y_t = move['x_t'], move['y_t']
+            x_t, y_t = move.x_t, move.y_t
 
         # Aim for the midpoint of the 1-light-year sector the goal is in.
-        dx, dy = Decimal(x_t).to_integral_value() - entity.x, Decimal(y_t).to_integral_value() - entity.y
+        try:
+            dx, dy = Decimal(x_t).to_integral_value() - move.actor.x, Decimal(y_t).to_integral_value() - move.actor.y
+        except Exception:
+            raise Exception(repr(move.actor.serialize()))
 
         D = Decimal(dx ** 2 + dy ** 2).sqrt()
         if D.to_integral_value() <= speed:
-            entity.dx, entity.dy = dx, dy
+            move.actor.dx, move.actor.dy = dx, dy
         else:
-            entity.dx, entity.dy = speed * dx / D, speed * dy / D
+            move.actor.dx, move.actor.dy = speed * dx / D, speed * dy / D
 
         # The naive prediction of the endpoint for this object
         remaining = self.N - self.step
-        x_p = (entity.x + remaining * entity.dx).to_integral_value()
-        y_p = (entity.y + remaining * entity.dy).to_integral_value()
-        if not hasattr(entity, 'x_p'):
-            entity.x_p, entity.y_p = x_p, y_p
+        x_p = (move.actor.x + remaining * move.actor.dx).to_integral_value()
+        y_p = (move.actor.y + remaining * move.actor.dy).to_integral_value()
+        if not hasattr(move.actor, 'x_p'):
+            move.actor.x_p, move.actor.y_p = x_p, y_p
         # If the projected endpoint is not stable, intercepting objects should just use Euler.
-        if (entity.x_p, entity.y_p) != (x_p, y_p):
-            entity.x_p, entity.y_p = None, None
+        if (move.actor.x_p, move.actor.y_p) != (x_p, y_p):
+            move.actor.x_p, move.actor.y_p = None, None
 
-    def _vector_to_projection(self, entity, manager):
+    def _vector_to_projection(self, move):
         # Update the real vector of this object based on the (non-updated) observable vector of its target.
-        move = entity.queue[0]
-        if 'target_id' not in move:
+        if move.target is None:
             return
 
-        target_entity = manager.get_entity('position', move['target_id'])
         # Only proceed with moving towards the target's projected endpoint if it is stable.
-        if getattr(target_entity, 'x_p', None) is None:
+        if getattr(move.target, 'x_p', None) is None:
             return
 
-        x_p, y_p = target_entity.x_p, target_entity.y_p
-        dx, dy = x_p - entity.x, y_p - entity.y
+        x_p, y_p = move.target.x_p, move.target.y_p
+        dx, dy = x_p - move.actor.x, y_p - move.actor.y
 
-        speed = Decimal(move['warp'] ** 2) / self.N
+        speed = Decimal(move.warp ** 2) / self.N
         D = Decimal(dx ** 2 + dy ** 2).sqrt()
         if D.to_integral_value() <= speed:
-            entity.dx, entity.dy = dx, dy
+            move.actor.dx, move.actor.dy = dx, dy
         else:
-            entity.dx, entity.dy = speed * dx / D, speed * dy / D
+            move.actor.dx, move.actor.dy = speed * dx / D, speed * dy / D
 
     def process(self, manager):
-        movements = {
-            _id: entity
-            for _id, entity in manager.get_entities('queue').items()
-            if entity.queue
-        }
+        movements = defaultdict(list)
+        for move in manager.get_entities('movement_orders').values():
+            movements[move.actor_id].append(move)
+
+        movements = {_id: sorted(queue, key=lambda x: x.seq) for _id, queue in movements.items() if queue}
 
         for _id, entity in manager.get_entities('position').items():
             entity.x_prev, entity.y_prev = entity.x, entity.y
 
         for self.step in range(self.N):
-            for entity in movements.values():
-                self._vector_to_target(entity, manager)
+            for queue in movements.values():
+                self._vector_to_target(queue[0])
 
-            for entity in movements.values():
-                self._vector_to_projection(entity, manager)
+            for queue in movements.values():
+                self._vector_to_projection(queue[0])
 
-            for entity in movements.values():
+            for queue in movements.values():
+                entity = queue[0].actor
                 dx, dy = entity.dx or Decimal(0), entity.dy or Decimal(0)
                 entity.x, entity.y = entity.x + dx, entity.y + dy
 
-        for _id, entity in movements.items():
+        for queue in movements.values():
+            entity = queue[0].actor
             entity.x, entity.y = int(entity.x.to_integral_value()), int(entity.y.to_integral_value())
 
         # drop any waypoints that have been reached
-        for _id, entity in manager.get_entities('queue').items():
-            if not entity.queue:
-                continue
-            move = entity.queue[0]
+        for queue in movements.values():
+            move = queue[0]
+            entity = move.actor
             x, y = entity.x, entity.y
-            if 'target_id' in move:
-                target_entity = manager.get_entity('position', move['target_id'])
-                x_t, y_t = target_entity.x, target_entity.y
+            if move.target is not None:
+                x_t, y_t = move.target.x, move.target.y
             else:
-                x_t, y_t = move['x_t'], move['y_t']
+                x_t, y_t = move.x_t, move.y_t
 
             if (x, y) == (x_t, y_t):
-                entity.queue.pop(0)
+                manager.unregister_entity(move)
+                queue.pop(0)
+                for i, order in enumerate(queue):
+                    order.seq = i
 
 
 class PopulationGrowthSystem:


Test patch:
diff --git a/tests/test_components.py b/tests/test_components.py
index ddb7c94..5e819ef 100644
--- a/tests/test_components.py
+++ b/tests/test_components.py
@@ -276,3 +276,101 @@ class OwnershipComponentTestCase(unittest.TestCase):
             planet1.validate()
 
         self.assertEqual(str(e.exception), "'owner_id' cannot point to an entity of this type.")
+
+
+class MovementComponentTestCase(unittest.TestCase):
+    def setUp(self):
+        self.manager = engine.Manager()
+        self.manager._entity_registry = {
+            'species': {
+                'metadata': components.MetadataComponent(),
+            },
+            'planet': {
+                'metadata': components.MetadataComponent(),
+                'ownership': components.OwnershipComponent(),
+            },
+            'ship': {
+                'metadata': components.MetadataComponent(),
+                'ownership': components.OwnershipComponent(),
+            },
+            'movement_order': {
+                'metadata': components.MetadataComponent(),
+                'orders': components.OrderComponent(),
+                'movement_orders': components.MovementComponent(),
+            },
+        }
+        engine.Entity.register_manager(self.manager)
+
+    def test_valid_to_coordinates(self):
+        species = self.manager.register_entity({'type': 'species'})
+        ship = self.manager.register_entity({'type': 'ship', 'owner_id': species.pk})
+        order = self.manager.register_entity(
+            {'type': 'movement_order', 'actor_id': ship.pk, 'seq': 0, 'warp': 10, 'x_t': 500, 'y_t': 500})
+
+        self.assertIsNone(order.validate())
+
+    def test_valid_to_target(self):
+        species = self.manager.register_entity({'type': 'species'})
+        planet = self.manager.register_entity({'type': 'planet'})
+        ship = self.manager.register_entity({'type': 'ship', 'owner_id': species.pk})
+        order = self.manager.register_entity(
+            {'type': 'movement_order', 'actor_id': ship.pk, 'seq': 0, 'warp': 10, 'target_id': planet.pk})
+
+        self.assertIsNone(order.validate())
+
+    def test_wrong_actor_type(self):
+        species = self.manager.register_entity({'type': 'species'})
+        planet = self.manager.register_entity({'type': 'planet', 'owner_id': species.pk})
+        order = self.manager.register_entity(
+            {'type': 'movement_order', 'actor_id': planet.pk, 'seq': 0, 'warp': 10, 'x_t': 500, 'y_t': 500})
+
+        with self.assertRaises(exceptions.ValidationError) as e:
+            order.validate()
+
+        self.assertEqual(str(e.exception), "The acting object must be a ship.")
+
+    def test_wrong_target_type(self):
+        species = self.manager.register_entity({'type': 'species'})
+        ship = self.manager.register_entity({'type': 'ship', 'owner_id': species.pk})
+        order = self.manager.register_entity(
+            {'type': 'movement_order', 'actor_id': ship.pk, 'seq': 0, 'warp': 10, 'target_id': species.pk})
+
+        with self.assertRaises(exceptions.ValidationError) as e:
+            order.validate()
+
+        self.assertEqual(str(e.exception), "'target_id' cannot point to an entity of this type.")
+
+    def test_no_goal(self):
+        species = self.manager.register_entity({'type': 'species'})
+        ship = self.manager.register_entity({'type': 'ship', 'owner_id': species.pk})
+        order = self.manager.register_entity(
+            {'type': 'movement_order', 'actor_id': ship.pk, 'seq': 0, 'warp': 10})
+
+        with self.assertRaises(exceptions.ValidationError) as e:
+            order.validate()
+
+        self.assertEqual(str(e.exception), "Either of 'target_id' or the target coordinates must be set.")
+
+    def test_two_goals(self):
+        species = self.manager.register_entity({'type': 'species'})
+        planet = self.manager.register_entity({'type': 'planet'})
+        ship = self.manager.register_entity({'type': 'ship', 'owner_id': species.pk})
+        order = self.manager.register_entity(
+            {'type': 'movement_order', 'actor_id': ship.pk, 'seq': 0, 'warp': 10,
+             'x_t': 500, 'y_t': 500, 'target_id': planet.pk})
+
+        with self.assertRaises(exceptions.ValidationError) as e:
+            order.validate()
+
+        self.assertEqual(str(e.exception), "Either of 'target_id' or the target coordinates must be set.")
+
+    def test_self_move(self):
+        species = self.manager.register_entity({'type': 'species'})
+        ship = self.manager.register_entity({'type': 'ship', 'owner_id': species.pk})
+        order = self.manager.register_entity(
+            {'type': 'movement_order', 'actor_id': ship.pk, 'seq': 0, 'warp': 10, 'target_id': ship.pk})
+
+        with self.assertRaises(exceptions.ValidationError) as e:
+            order.validate()
+
+        self.assertEqual(str(e.exception), "A ship cannot target itself for a movement order.")
diff --git a/tests/test_engine.py b/tests/test_engine.py
index 869f417..b5432f1 100644
--- a/tests/test_engine.py
+++ b/tests/test_engine.py
@@ -6,8 +6,7 @@ from universe import engine, exceptions
 class EntityTestCase(unittest.TestCase):
     def test_invalid_ownership(self):
         state = {
-            'turn': 2500,
-            'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 2,
             'entities': [
                 {
                     'pk': 1,
@@ -23,7 +22,6 @@ class EntityTestCase(unittest.TestCase):
                     'ironium': 20,
                     'boranium': 30,
                     'germanium': 40,
-                    'queue': [],
                     'owner_id': 0,  # Not an entity in the pool.
                     'population': 1000,
                 }
@@ -40,8 +38,7 @@ class EntityTestCase(unittest.TestCase):
 class PersistenceTestCase(unittest.TestCase):
     def test_planet(self):
         state = {
-            'turn': 2500,
-            'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 2,
             'entities': [
                 {
                     'pk': 0,
@@ -67,7 +64,6 @@ class PersistenceTestCase(unittest.TestCase):
                     'ironium': 20,
                     'boranium': 30,
                     'germanium': 40,
-                    'queue': [],
                     'owner_id': 0,
                     'population': 1000,
                 }
@@ -92,14 +88,12 @@ class PersistenceTestCase(unittest.TestCase):
         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,
+            'turn': 2500, 'width': 1000, 'seq': 2,
             'entities': [
                 {
                     'pk': 0,
@@ -119,7 +113,6 @@ class PersistenceTestCase(unittest.TestCase):
                     'ironium': 20,
                     'boranium': 30,
                     'germanium': 40,
-                    'queue': [],
                     'owner_id': 0,
                     'population': 1000,
                 }
@@ -138,14 +131,12 @@ class PersistenceTestCase(unittest.TestCase):
         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,
+            'turn': 2500, 'width': 1000, 'seq': 1,
             'entities': [
                 {
                     'pk': 0,
@@ -178,34 +169,32 @@ class PersistenceTestCase(unittest.TestCase):
 class MovementTestCase(unittest.TestCase):
     def test_empty_universe(self):
         state = {'turn': 2500, 'width': 1000, 'entities': []}
-        updates = {}
 
-        S = engine.GameState(state, updates)
+        S = engine.GameState(state, [])
         results = S.generate()
 
         self.assertEqual(results,
                          {'turn': 2501, 'width': 1000, 'seq': 0, 'entities': []})
 
     def test_one_stationary_object(self):
-        state = {'turn': 2500, 'width': 1000,
-                 'entities': [{'pk': 0, 'type': 'ship', 'x': 456, 'y': 337, 'queue': []}]}
-        updates = {}
+        state = {'turn': 2500, 'width': 1000, 'seq': 1,
+                 'entities': [{'pk': 0, 'type': 'ship', 'x': 456, 'y': 337}]}
 
-        S = engine.GameState(state, updates)
+        S = engine.GameState(state, [])
         results = S.generate()
 
         self.assertEqual(results['turn'], 2501)
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            [{'pk': 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}]
         )
 
     def test_one_object_single_turn_move(self):
-        state = {'turn': 2500, 'width': 1000,
-                 'entities': [{'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': []}]}
+        state = {'turn': 2500, 'width': 1000, 'seq': 1,
+                 'entities': [{'pk': 0, 'type': 'ship', 'x': 480, 'y': 235}]}
         # actual speed is warp**2
-        updates = {0: [{'seq': 0, 'x_t': 422, 'y_t': 210, 'warp': 10}]}
+        updates = [{'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'x_t': 422, 'y_t': 210, 'warp': 10}]
 
         S = engine.GameState(state, updates)
         results = S.generate()
@@ -214,14 +203,14 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            [{'pk': 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}]
         )
 
     def test_multi_turn_single_move(self):
-        state = {'turn': 2500, 'width': 1000,
-                 'entities': [{'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': []}]}
+        state = {'turn': 2500, 'width': 1000, 'seq': 1,
+                 'entities': [{'pk': 0, 'type': 'ship', 'x': 480, 'y': 235}]}
         # actual speed is warp**2
-        updates = {0: [{'seq': 0, 'x_t': 168, 'y_t': 870, 'warp': 10}]}
+        updates = [{'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'x_t': 168, 'y_t': 870, 'warp': 10}]
 
         S = engine.GameState(state, updates)
         results = S.generate()
@@ -230,37 +219,38 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            [{'pk': 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},
+             {'pk': 1, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'x_t': 168, 'y_t': 870, 'warp': 10}]
         )
 
     def test_previously_queued_move(self):
         state = {
-            'turn': 2500, 'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 2,
             'entities': [
-                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'x_t': 422, 'y_t': 210, 'warp': 10}]}
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235},
+                {'pk': 1, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'x_t': 422, 'y_t': 210, 'warp': 10},
             ]
         }
-        updates = {}
 
-        S = engine.GameState(state, updates)
+        S = engine.GameState(state, [])
         results = S.generate()
 
         self.assertEqual(results['turn'], 2501)
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            [{'pk': 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}]
         )
 
     def test_replace_queued_move(self):
         state = {
-            'turn': 2500, 'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 2,
             'entities': [
-                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'x_t': 637, 'y_t': 786, 'warp': 8}]}
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235},
+                {'pk': 1, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8},
             ]
         }
-        updates = {0: [{'seq': 0, 'x_t': 422, 'y_t': 210, 'warp': 10}]}
+        updates = [{'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'x_t': 422, 'y_t': 210, 'warp': 10}]
 
         S = engine.GameState(state, updates)
         results = S.generate()
@@ -269,17 +259,18 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            [{'pk': 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}]
         )
 
     def test_add_to_queued_moves(self):
         state = {
-            'turn': 2500, 'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 2,
             'entities': [
-                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'x_t': 422, 'y_t': 210, 'warp': 10}]}
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235},
+                {'pk': 1, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'x_t': 422, 'y_t': 210, 'warp': 10},
             ]
         }
-        updates = {0: [{'seq': 1, 'x_t': 637, 'y_t': 786, 'warp': 8}]}
+        updates = [{'type': 'movement_order', 'actor_id': 0, 'seq': 1, 'x_t': 637, 'y_t': 786, 'warp': 8}]
 
         S = engine.GameState(state, updates)
         results = S.generate()
@@ -288,15 +279,16 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            [{'pk': 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},
+             {'pk': 2, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'x_t': 637, 'y_t': 786, 'warp': 8}]
         )
 
     def test_slow_motion(self):
         state = {
-            'turn': 2500, 'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 2,
             'entities': [
-                {'pk': 0, 'type': 'ship', 'x': 500, 'y': 500, 'queue': [{'x_t': 501, 'y_t': 500, 'warp': 1}]}
+                {'pk': 0, 'type': 'ship', 'x': 500, 'y': 500},
+                {'pk': 1, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'x_t': 501, 'y_t': 500, 'warp': 1},
             ]
         }
 
@@ -307,57 +299,60 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            [{'pk': 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}]
         )
 
     def test_target_stationary_object(self):
         state = {
-            'turn': 2500, 'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 3,
             'entities': [
-                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235, 'queue': [{'target_id': 1, 'warp': 10}]},
-                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215, 'queue': []}
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235},
+                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215},
+                {'pk': 2, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'target_id': 1, 'warp': 10},
             ]
         }
-        updates = {}
 
-        S = engine.GameState(state, updates)
+        S = engine.GameState(state, [])
         results = S.generate()
 
         self.assertEqual(results['turn'], 2501)
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            [{'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': []}]
+            [{'pk': 0, 'type': 'ship', 'x': 460, 'x_prev': 480, 'y': 215, 'y_prev': 235},
+             {'pk': 1, 'type': 'ship', 'x': 460, 'x_prev': 460, 'y': 215, 'y_prev': 215}]
         )
 
     def test_target_moving_object(self):
         state = {
-            'turn': 2500, 'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 4,
             '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}]}
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235},
+                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215},
+                {'pk': 2, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'target_id': 1, 'warp': 10},
+                {'pk': 3, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 465, 'y_t': 220, 'warp': 10},
             ]
         }
-        updates = {}
 
-        S = engine.GameState(state, updates)
+        S = engine.GameState(state, [])
         results = S.generate()
 
         self.assertEqual(results['turn'], 2501)
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            [{'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': []}]
+            [{'pk': 0, 'type': 'ship', 'x': 465, 'x_prev': 480, 'y': 220, 'y_prev': 235},
+             {'pk': 1, 'type': 'ship', 'x': 465, 'x_prev': 460, 'y': 220, 'y_prev': 215}]
         )
 
     def test_target_can_barely_reach(self):
         state = {
-            'turn': 2500, 'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 4,
             '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}]}
+                {'pk': 0, 'type': 'ship', 'x': 560, 'y': 315},
+                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215},
+                {'pk': 2, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'target_id': 1, 'warp': 10},
+                {'pk': 3, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 660, 'y_t': 215, 'warp': 10},
             ]
         }
         S = engine.GameState(state, {})
@@ -367,17 +362,19 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(results['width'], 1000)
         self.assertEqual(
             results['entities'],
-            [{'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}]}]
+            [{'pk': 0, 'type': 'ship', 'x': 560, 'x_prev': 560, 'y': 215, 'y_prev': 315},
+             {'pk': 1, 'type': 'ship', 'x': 560, 'x_prev': 460, 'y': 215, 'y_prev': 215},
+             {'pk': 3, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 660, 'y_t': 215, 'warp': 10}]
         )
 
     def test_target_stops_short(self):
         state = {
-            'turn': 2500, 'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 4,
             '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}]}
+                {'pk': 0, 'type': 'ship', 'x': 560, 'y': 315},
+                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215},
+                {'pk': 2, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'target_id': 1, 'warp': 10},
+                {'pk': 3, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'x_t': 510, 'y_t': 215, 'warp': 10},
             ]
         }
         S = engine.GameState(state, {})
@@ -389,22 +386,23 @@ class MovementTestCase(unittest.TestCase):
         # i.e. roughly 35 lightyears in each direction
         self.assertEqual(
             results['entities'],
-            [{'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': []}]
+            [{'pk': 0, 'type': 'ship', 'x': 524, 'x_prev': 560, 'y': 230, 'y_prev': 315},
+             {'pk': 1, 'type': 'ship', 'x': 510, 'x_prev': 460, 'y': 215, 'y_prev': 215},
+             {'pk': 2, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'target_id': 1, 'warp': 10}]
         )
 
     def test_mutual_intercept(self):
         state = {
-            'turn': 2500, 'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 4,
             '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}]}
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235},
+                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215},
+                {'pk': 2, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'target_id': 1, 'warp': 10},
+                {'pk': 3, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'target_id': 0, 'warp': 10},
             ]
         }
-        updates = {}
 
-        S = engine.GameState(state, updates)
+        S = engine.GameState(state, [])
         results = S.generate()
 
         self.assertEqual(results['turn'], 2501)
@@ -412,21 +410,21 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(len(results['entities']), 2)
         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,
+            'turn': 2500, 'width': 1000, 'seq': 6,
             '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}]}
+                {'pk': 0, 'type': 'ship', 'x': 480, 'y': 235},
+                {'pk': 1, 'type': 'ship', 'x': 460, 'y': 215},
+                {'pk': 2, 'type': 'ship', 'x': 500, 'y': 205},
+                {'pk': 3, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'target_id': 1, 'warp': 10},
+                {'pk': 4, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'target_id': 2, 'warp': 10},
+                {'pk': 5, 'type': 'movement_order', 'actor_id': 2, 'seq': 0, 'target_id': 0, 'warp': 10},
             ]
         }
-        updates = {}
 
-        S = engine.GameState(state, updates)
+        S = engine.GameState(state, [])
         results = S.generate()
 
         self.assertEqual(results['turn'], 2501)
@@ -434,21 +432,23 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(len(results['entities']), 3)
         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']))
 
     def test_four_way_cycle_intercept(self):
         state = {
-            'turn': 2500, 'width': 1000,
+            'turn': 2500, 'width': 1000, 'seq': 8,
             '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}]}
+                {'pk': 0, 'type': 'ship', 'x': 500, 'y': 500},
+                {'pk': 1, 'type': 'ship', 'x': 600, 'y': 500},
+                {'pk': 2, 'type': 'ship', 'x': 600, 'y': 600},
+                {'pk': 3, 'type': 'ship', 'x': 500, 'y': 600},
+                {'pk': 4, 'type': 'movement_order', 'actor_id': 0, 'seq': 0, 'target_id': 1, 'warp': 10},
+                {'pk': 5, 'type': 'movement_order', 'actor_id': 1, 'seq': 0, 'target_id': 2, 'warp': 10},
+                {'pk': 6, 'type': 'movement_order', 'actor_id': 2, 'seq': 0, 'target_id': 3, 'warp': 10},
+                {'pk': 7, 'type': 'movement_order', 'actor_id': 3, 'seq': 0, 'target_id': 0, 'warp': 10},
             ]
         }
-        updates = {}
 
-        S = engine.GameState(state, updates)
+        S = engine.GameState(state, [])
         results = S.generate()
 
         self.assertEqual(results['turn'], 2501)
@@ -456,4 +456,3 @@ class MovementTestCase(unittest.TestCase):
         self.assertEqual(len(results['entities']), 4)
         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']))
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 058dc95..31b6567 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -99,36 +99,6 @@ class IntFieldTestCase(unittest.TestCase):
         self.assertEqual(str(e.exception), "'widgets' must be less than or equal to 100.")
 
 
-class ListFieldTestCase(unittest.TestCase):
-    def test_not_required(self):
-        field = fields.ListField(required=False)
-        field.name = 'queue'
-
-        self.assertIsNone(field.validate({}))
-
-    def test_explicit_required(self):
-        field = fields.ListField(required=True)
-        field.name = 'queue'
-
-        with self.assertRaises(exceptions.ValidationError) as e:
-            field.validate({})
-        self.assertEqual(str(e.exception), "'queue' is required.")
-
-    def test_bad_type(self):
-        field = fields.ListField(required=True)
-        field.name = 'queue'
-
-        with self.assertRaises(exceptions.ValidationError) as e:
-            field.validate({'queue': 0})
-        self.assertEqual(str(e.exception), "'queue' must be a list.")
-
-    def test_empty_list_is_valid(self):
-        field = fields.ListField(required=True)
-        field.name = 'queue'
-
-        self.assertIsNone(field.validate({'queue': []}))
-
-
 class CharFieldTestCase(unittest.TestCase):
     def test_not_required(self):
         field = fields.CharField(required=False)
diff --git a/tests/test_systems.py b/tests/test_systems.py
index c5df266..052b19b 100644
--- a/tests/test_systems.py
+++ b/tests/test_systems.py
@@ -33,7 +33,6 @@ class PopulationTestCase(unittest.TestCase):
                     'boranium_conc': 50,
                     'germanium_conc': 50,
                     'owner_id': 0,
-                    'queue': [],
                 },
             ]
         }
@@ -73,7 +72,6 @@ class PopulationTestCase(unittest.TestCase):
                     'boranium_conc': 50,
                     'germanium_conc': 50,
                     'owner_id': 0,
-                    'queue': [],
                 },
             ]
         }
@@ -122,7 +120,6 @@ class PopulationTestCase(unittest.TestCase):
                     'boranium_conc': 50,
                     'germanium_conc': 50,
                     'owner_id': 0,
-                    'queue': [],
                 },
             ]
         }
@@ -168,7 +165,6 @@ class PopulationTestCase(unittest.TestCase):
                     'boranium_conc': 50,
                     'germanium_conc': 50,
                     'owner_id': 0,
-                    'queue': [],
                 },
             ]
         }
@@ -216,7 +212,6 @@ class PopulationTestCase(unittest.TestCase):
                     'boranium_conc': 50,
                     'germanium_conc': 50,
                     'owner_id': 0,
-                    'queue': [],
                 },
             ]
         }
@@ -258,7 +253,6 @@ class PopulationTestCase(unittest.TestCase):
                     'boranium_conc': 50,
                     'germanium_conc': 50,
                     'owner_id': 0,
-                    'queue': [],
                 },
             ]
         }