Repository: ocadotechnology/aimmo

Issue:
Change the way the UI receives game updates
The UI currently polls the frontend server constantly for updates.

Implement a solution where the _game simulation_ server pushes updates instead - e.g. using web sockets (or some other better solution that you can think of!)

Possible server-side solutions:
1. client ==> proxy (on Kubernetes) ==> game simulation (on Kubernetes)
Here we only need one public IP address for the proxy, no matter how many game simulation servers we spawn.
1. client ==> game simulation (on Kubernetes)
   We need a public IP address per game simulation. We need to investigate whether this would be a problem (e.g. can't be done dynamically, or is expensive).



Discussion:

Resolving patch:
diff --git a/aimmo-game/service.py b/aimmo-game/service.py
--- a/aimmo-game/service.py
+++ b/aimmo-game/service.py
@@ -1,8 +1,11 @@
 #!/usr/bin/env python
 import logging
 
+import eventlet
+eventlet.monkey_patch()
+
 import flask
-from flask.ext.cors import CORS
+from flask.ext.socketio import SocketIO, emit
 
 from simulation.turn_manager import world_state_provider
 from simulation import map_generator
@@ -12,7 +15,7 @@
 from simulation.worker_manager import LocalWorkerManager
 
 app = flask.Flask(__name__)
-CORS(app)
+socketio = SocketIO(app)
 
 
 def to_cell_type(cell):
@@ -42,7 +45,6 @@ def player_dict(avatar):
     }
 
 
-@app.route('/')
 def get_world_state():
     try:
         world = world_state_provider.lock_and_get_world()
@@ -52,25 +54,41 @@ def get_world_state():
         for cell in world.world_map.all_cells():
             grid[cell.location.x][cell.location.y] = to_cell_type(cell)
         player_data = {p.player_id: player_dict(p) for p in world.avatar_manager.avatars}
-        return flask.jsonify(
-            players=player_data,
-            score_locations=[(cell.location.x, cell.location.y) for cell in world.world_map.score_cells()],
-            pickup_locations=[(cell.location.x, cell.location.y) for cell in world.world_map.pickup_cells()],
-            map_changed=True,  # TODO: experiment with only sending deltas (not if not required)
-            width=num_cols,
-            height=num_rows,
-            layout=grid,
-        )
+        return {
+                'players': player_data,
+                'score_locations': [(cell.location.x, cell.location.y) for cell in world.world_map.score_cells()],
+                'pickup_locations': [(cell.location.x, cell.location.y) for cell in world.world_map.pickup_cells()],
+                'map_changed': True,  # TODO: experiment with only sending deltas (not if not required)
+                'width': num_cols,
+                'height': num_rows,
+                'layout': grid,
+            }
     finally:
         world_state_provider.release_lock()
 
 
+@socketio.on('connect')
+def world_update_on_connect():
+    emit(
+        'world-update',
+        get_world_state(),
+    )
+
+
+def send_world_update():
+    socketio.emit(
+        'world-update',
+        get_world_state(),
+        broadcast=True,
+    )
+
+
 def run_game():
     print("Running game...")
     my_map = map_generator.generate_map(15, 15, 0.1)
     player_manager = AvatarManager()
     game_state = GameState(my_map, player_manager)
-    turn_manager = TurnManager(game_state=game_state)
+    turn_manager = TurnManager(game_state=game_state, end_turn_callback=send_world_update)
     worker_manager = LocalWorkerManager(game_state=game_state, users_url='http://localhost:8000/players/api/games/')
     worker_manager.start()
     turn_manager.start()
@@ -80,4 +98,4 @@ def run_game():
     logging.basicConfig(level=logging.DEBUG)
 
     run_game()
-    app.run(debug=True, use_reloader=False)
+    socketio.run(app, debug=True, use_reloader=False)
diff --git a/aimmo-game/simulation/turn_manager.py b/aimmo-game/simulation/turn_manager.py
--- a/aimmo-game/simulation/turn_manager.py
+++ b/aimmo-game/simulation/turn_manager.py
@@ -41,8 +41,9 @@ class TurnManager(threading.Thread):
     """
     daemon = True
 
-    def __init__(self, game_state):
+    def __init__(self, game_state, end_turn_callback):
         world_state_provider.set_world(game_state)
+        self.end_turn_callback = end_turn_callback
         super(TurnManager, self).__init__()
 
     def _update_environment(self, game_state):
@@ -76,4 +77,5 @@ def run_turn(self):
     def run(self):
         while True:
             self.run_turn()
+            self.end_turn_callback()
             time.sleep(0.5)
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -8,8 +8,9 @@
         'django >= 1.8.3, < 1.9.0',
         'django-autoconfig >= 0.3.6, < 1.0.0',
         'django-js-reverse',
+        'eventlet',
         'flask',
-        'flask-cors',
+        'flask-socketio',
         'requests',
     ],
     tests_require=[


Test patch: