diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fd3b41a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM rust:1.44 AS build-env + + +WORKDIR /sources + +RUN git clone -b wasm32-target-fix https://github.com/drager/wasm-pack.git +WORKDIR wasm-pack + +RUN rustup default nightly +RUN cargo install --path . + + +WORKDIR /planetwars +COPY . . + +WORKDIR backend +RUN cargo build --release + + +WORKDIR ../frontend + +RUN cargo update +RUN wasm-pack build + +FROM node:10 +COPY --from=build-env /planetwars /planetwars +WORKDIR /planetwars/frontend/www +RUN npm install +RUN npm run build + +WORKDIR /planetwars/backend + +EXPOSE 9142 +EXPOSE 8123 +EXPOSE 3012 + +CMD ["target/release/planetwars"] + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e263b3 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Planetwars + +## Docker + +Build: `docker build --tag pw:1.0 .` + +Run: `docker run --rm -p 8123:8123 -p 9142:9142 -p 3012:3012 -v $(pwd)/backend/games:/planetwars/backend/games --name planetwars pw:1.0` + +Add parameter `-e ROCKET_HOST_NAME=`, for example `-e ROCKET_HOST_NAME=mozaic.zeus.gent`, to set the domain used in the application. diff --git a/backend/README.md b/backend/README.md index e22697f..68f954d 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,3 +1,5 @@ # Planetwars backend +Change hostname in info slides with ROCKET_HOST_NAME env variable, or in `Rocket.toml`. + The main planetwars server that instanciates planetwars matches etc... diff --git a/backend/Rocket.toml b/backend/Rocket.toml index fcec49c..c4f0ef0 100644 --- a/backend/Rocket.toml +++ b/backend/Rocket.toml @@ -1,6 +1,7 @@ [development] address = "localhost" port = 8000 +host_name = "localhost" [staging] address = "0.0.0.0" @@ -9,3 +10,4 @@ port = 80 [production] address = "0.0.0.0" port = 8123 +host_name = "mozaic.zeus.gent" diff --git a/backend/games/.keep b/backend/games/.keep new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/main.rs b/backend/src/main.rs index fb6c5aa..74f2ef8 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,4 +1,4 @@ -#![feature(proc_macro_hygiene)] +#![feature(proc_macro_hygiene, async_closure)] extern crate serde; #[macro_use] @@ -40,6 +40,7 @@ mod util; use util::Games; use util::COLOURS; +use rocket::fairing::AdHoc; use rocket_contrib::templates::tera::{self, Value}; use rocket_contrib::templates::{Engines, Template}; @@ -85,6 +86,14 @@ fn get_colour(value: &Value, _: &HashMap) -> tera::Result )); } +struct HostNameFilter(String); + +impl tera::Filter for HostNameFilter { + fn filter(&self, _: &Value, _: &HashMap) -> tera::Result { + Ok(Value::String(self.0.clone())) + } +} + /// Async main function, starting logger, graph and rocket #[launch] async fn rocket() -> rocket::Rocket { @@ -103,16 +112,25 @@ async fn rocket() -> rocket::Rocket { let mut routes = Vec::new(); routes::fuel(&mut routes); - let tera = Template::custom(|engines: &mut Engines| { - engines.tera.register_filter("calc_viewbox", calc_viewbox); - engines.tera.register_filter("get_colour", get_colour); - }); - rocket::ignite() .manage(gm) .manage(pool) .manage(Games::new()) - .attach(tera) + .attach(AdHoc::on_attach("Assets Config", async move |mut rocket| { + let host_name = rocket.config().await + .get_str("host_name") + .unwrap_or("mozaic.zeus.gent") + .to_string(); + + let tera = Template::custom(move |engines: &mut Engines| { + let filter = HostNameFilter(host_name.clone()); + engines.tera.register_filter("calc_viewbox", calc_viewbox); + engines.tera.register_filter("get_colour", get_colour); + engines.tera.register_filter("get_host_name", filter); + }); + + Ok(rocket.attach(tera)) + })) .mount("/", routes) } @@ -125,5 +143,5 @@ async fn create_game_manager(tcp: &str, pool: ThreadPool) -> game::Manager { let ep = TcpEndpoint::new(addr, pool.clone()); let gmb = gmb.add_endpoint(ep, "TCP endpoint"); - gmb.build("games.ini", pool).await.unwrap() + gmb.build("games/games.json", pool).await.unwrap() } diff --git a/backend/src/planetwars/mod.rs b/backend/src/planetwars/mod.rs index 7da6dab..5da86dc 100644 --- a/backend/src/planetwars/mod.rs +++ b/backend/src/planetwars/mod.rs @@ -36,11 +36,11 @@ impl PlanetWarsGame { .map(|p| (p.name.clone(), p.id)) .collect(); - if let Err(_) = create_dir("static/games") { - println!("'static/games' already exists"); + if let Err(_) = create_dir("games") { + println!("'games' already exists"); } - let file = File::create(format!("static/games/{}", location)).unwrap(); + let file = File::create(format!("games/{}", location)).unwrap(); Self { state, diff --git a/backend/src/routes/mod.rs b/backend/src/routes/mod.rs index ec12ea2..d4741c2 100644 --- a/backend/src/routes/mod.rs +++ b/backend/src/routes/mod.rs @@ -20,7 +20,13 @@ mod maps; /// Handles all files located in the static folder #[get("/", rank = 6)] async fn files(file: PathBuf) -> Option { - NamedFile::open(Path::new("static/").join(file)).ok() + NamedFile::open(Path::new("static/").join(file)).await.ok() +} + +/// Handles all games files to be served +#[get("/games/")] +async fn game_get(loc: String) -> Option { + NamedFile::open(Path::new("games/").join(loc)).await.ok() } /// Routes the index page, rendering the index Template. @@ -62,6 +68,7 @@ pub fn fuel(routes: &mut Vec) { routes.extend(routes![ files, index, + game_get, builder_get, visualizer_get, debug_get @@ -71,18 +78,20 @@ pub fn fuel(routes: &mut Vec) { info::fuel(routes); } -/// Reads games.ini +/// Reads games.json /// File that represents all played games /// Ready to be visualized async fn get_played_games() -> Vec { - match fs::File::open("games.ini").await { + match fs::File::open("games/games.json").await { Ok(file) => { let file = BufReader::new(file); file.lines() - .filter_map(move |maybe| async { - maybe - .ok() - .and_then(|line| serde_json::from_str::(&line).ok()) + .filter_map(move |maybe| { + async { + maybe + .ok() + .and_then(|line| serde_json::from_str::(&line).ok()) + } }) .map(|state| state.into()) .collect() diff --git a/backend/templates/info/info_6.html.tera b/backend/templates/info/info_6.html.tera index 8e2830d..a949a76 100644 --- a/backend/templates/info/info_6.html.tera +++ b/backend/templates/info/info_6.html.tera @@ -16,9 +16,9 @@ How to connect
         
-$ wget mozaic.zeus.gent/bot/runner.py
-$ wget mozaic.zeus.gent/bot/simple.py
-$ python3 runner.py -p 9142 --host mozaic.zeus.gent \
+$ wget {{ "" | get_host_name}}/bot/runner.py
+$ wget {{ "" | get_host_name}}/bot/simple.py
+$ python3 runner.py -p 9142 --host {{ "" | get_host_name}}\
     -n <Your name> -i <Id from the lobby> \
     python3 simple.py
         
diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs
index 331931b..8dc5dd4 100644
--- a/frontend/src/lib.rs
+++ b/frontend/src/lib.rs
@@ -1,15 +1,15 @@
 extern crate serde;
 #[macro_use]
 extern crate serde_derive;
-extern crate serde_json;
 extern crate octoon_math;
+extern crate serde_json;
 extern crate voronoi;
 
-use octoon_math::{Mat3, Vec3, Vec2};
-use voronoi::{Point, voronoi, make_polygons};
+use octoon_math::Mat3;
+use voronoi::{make_polygons, voronoi, Point};
 
-mod utils;
 mod types;
+mod utils;
 
 use std::collections::HashMap;
 use wasm_bindgen::prelude::*;
@@ -38,7 +38,7 @@ pub struct Circle {
 
 use std::f32::consts::PI;
 fn spr(from: f32) -> f32 {
-    let pi2 = PI*2.;
+    let pi2 = PI * 2.;
     ((from % pi2) + pi2) % pi2
 }
 
@@ -50,17 +50,17 @@ impl Circle {
         let y2 = p2.y;
 
         // Distance between planets
-        let q = ((x2-x1).powi(2) + (y2-y1).powi(2)).sqrt();
+        let q = ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt();
         // Center of between planets
-        let x3 = (x1+x2)/2.0;
-        let y3 = (y1+y2)/2.0;
+        let x3 = (x1 + x2) / 2.0;
+        let y3 = (y1 + y2) / 2.0;
 
         // Radius of circle
         let r = q * 1.0;
 
         // Center of circle
-        let x = x3 + (r.powi(2)-(q/2.0).powi(2)).sqrt() * (y1-y2)/q;
-        let y = y3 + (r.powi(2)-(q/2.0).powi(2)).sqrt() * (x2-x1)/q;
+        let x = x3 + (r.powi(2) - (q / 2.0).powi(2)).sqrt() * (y1 - y2) / q;
+        let y = y3 + (r.powi(2) - (q / 2.0).powi(2)).sqrt() * (x2 - x1) / q;
         // console_log!("{},{} -> {},{} ({},{} r={})", x1, y1, x2, y2, x, y, r);
 
         let a0 = spr((y - y1).atan2(x - x1));
@@ -74,7 +74,12 @@ impl Circle {
 
         let distance = q.ceil() as usize + 1;
         Self {
-            r, x, y, a0, ad, distance
+            r,
+            x,
+            y,
+            a0,
+            ad,
+            distance,
         }
     }
 
@@ -90,19 +95,31 @@ impl Circle {
 
         let cos = alpha.cos();
         let sin = alpha.sin();
-        (Mat3::new(
-            0.3, 0.0, 0.0,
-            0.0, 0.3, 0.0,
-            -self.x + cos * self.r, -self.y + sin * self.r, 0.3,
-        ), alpha)
+        (
+            Mat3::new(
+                0.3,
+                0.0,
+                0.0,
+                0.0,
+                0.3,
+                0.0,
+                -self.x + cos * self.r,
+                -self.y + sin * self.r,
+                0.3,
+            ),
+            alpha,
+        )
     }
 }
 
-fn create_voronoi(planets: &Vec, bbox: f32) -> (Vec>, Vec) {
-    let mut verts: Vec> = planets.iter().map(|p| Vec2::new(p.x, p.y)).collect();
+fn create_voronoi(planets: &Vec, bbox: f32) -> (Vec, Vec) {
+    let mut verts: Vec<[f32; 2]> = planets.iter().map(|p| [p.x, p.y]).collect();
     let mut ids = Vec::new();
 
-    let vor_points = planets.iter().map(|p| Point::new(p.x as f64, p.y as f64)).collect();
+    let vor_points = planets
+        .iter()
+        .map(|p| Point::new(p.x as f64, p.y as f64))
+        .collect();
 
     let vor = voronoi(vor_points, bbox as f64);
     let vor = make_polygons(&vor);
@@ -113,9 +130,8 @@ fn create_voronoi(planets: &Vec, bbox: f32) -> (Vec>, V
 
         let mut prev = ids.len() + poly.len() - 1;
         for p in poly.iter() {
-
             let now = verts.len();
-            verts.push(Vec2::new(p.x.0 as f32, p.y.0 as f32));
+            verts.push([p.x.0 as f32, p.y.0 as f32]);
 
             ids.push(idx);
             ids.push(now);
@@ -124,10 +140,9 @@ fn create_voronoi(planets: &Vec, bbox: f32) -> (Vec>, V
         }
     }
 
-    (verts, ids)
+    (verts.concat(), ids)
 }
 
-
 #[wasm_bindgen]
 pub struct Game {
     states: Vec,
@@ -138,18 +153,18 @@ pub struct Game {
     /* put extra shit here */
     view_box: Vec,
 
-    planets: Vec>,
+    planets: Vec,
     planet_ships: Vec,
 
-    ship_locations: Vec<[f32;9]>,
-    ship_label_locations: Vec<[f32;9]>,
-    ship_colours: Vec>,
+    ship_locations: Vec,
+    ship_label_locations: Vec,
+    ship_colours: Vec,
     ship_counts: Vec,
 
-    current_planet_colours: Vec>,
+    current_planet_colours: Vec,
 
-    voronoi_vertices: Vec>,
-    voronoi_colors: Vec>,
+    voronoi_vertices: Vec,
+    voronoi_colors: Vec,
     voronoi_indices: Vec,
 }
 
@@ -161,9 +176,10 @@ impl Game {
         console_log!("Rust is busy being awesome!");
 
         // First line is fucked but we just filter out things that cannot parse
-        let states: Vec = file.split("\n").filter_map(|line|
-            serde_json::from_str(line).ok()
-        ).collect();
+        let states: Vec = file
+            .split("\n")
+            .filter_map(|line| serde_json::from_str(line).ok())
+            .collect();
 
         let mut planet_map = HashMap::new();
 
@@ -175,10 +191,14 @@ impl Game {
         }
         let view_box = utils::caclulate_viewbox(&states[0].planets);
 
+        let (voronoi_vertices, voronoi_indices) =
+            create_voronoi(&states[0].planets, view_box[2].max(view_box[3]));
 
-        let (voronoi_vertices, voronoi_indices) = create_voronoi(&states[0].planets, view_box[2].max(view_box[3]));
-
-        let voronoi_colors = voronoi_indices.iter().map(|_| Vec3::new(0.0, 0.0, 0.0)).collect(); // Init these colours on black
+        let voronoi_colors: Vec = voronoi_indices
+            .iter()
+            .map(|_| [0.0, 0.0, 0.0])
+            .collect::>()
+            .concat(); // Init these colours on black
 
         Self {
             planets: utils::get_planets(&states[0].planets, 2.0),
@@ -200,24 +220,20 @@ impl Game {
         }
     }
 
-    pub fn get_viewbox(&self) -> *const f32 {
-        self.view_box.as_ptr()
+    pub fn get_viewbox(&self) -> Vec {
+        self.view_box.clone()
     }
 
-    pub fn get_planets(&self) -> *const Vec3 {
-        self.planets.as_ptr()
+    pub fn get_planets(&self) -> Vec {
+        self.planets.clone()
     }
 
-    pub fn get_planet_ships(&self) -> *const usize {
-        self.planet_ships.as_ptr()
+    pub fn get_planet_ships(&self) -> Vec {
+        self.planet_ships.clone()
     }
 
-    pub fn get_planet_colors(&self) -> *const Vec3 {
-        self.current_planet_colours.as_ptr()
-    }
-
-    pub fn get_planet_count(&self) -> usize {
-        self.planets.len()
+    pub fn get_planet_colors(&self) -> Vec {
+        self.current_planet_colours.clone()
     }
 
     pub fn turn_count(&self) -> usize {
@@ -225,7 +241,7 @@ impl Game {
     }
 
     pub fn update_turn(&mut self, turn: usize) -> usize {
-        self.turn = turn.min(self.states.len() -1);
+        self.turn = turn.min(self.states.len() - 1);
 
         self.update_planet_ships();
         self.update_planet_colours();
@@ -237,105 +253,114 @@ impl Game {
     }
 
     fn update_planet_ships(&mut self) {
-        self.planet_ships = self.states[self.turn].planets.iter().map(|p| p.ship_count as usize).collect();
+        self.planet_ships = self.states[self.turn]
+            .planets
+            .iter()
+            .map(|p| p.ship_count as usize)
+            .collect();
     }
 
     fn update_voronoi_colors(&mut self) {
         for (i, p) in self.states[self.turn].planets.iter().enumerate() {
-            self.voronoi_colors[i] = utils::COLORS[p.owner.unwrap_or(0) as usize % utils::COLORS.len()].into()
+            let color = utils::COLORS[p.owner.unwrap_or(0) as usize % utils::COLORS.len()];
+            self.voronoi_colors[i * 3 + 0] = color[0];
+            self.voronoi_colors[i * 3 + 1] = color[1];
+            self.voronoi_colors[i * 3 + 2] = color[2];
         }
     }
 
     fn update_planet_colours(&mut self) {
-        let mut new_vec = Vec::new();
+        let mut new_vec: Vec<[f32; 3]> = Vec::new();
         let planets_now = self.states[self.turn].planets.iter();
-        let planets_later = self.states[(self.turn + 1).min(self.states.len() - 1)].planets.iter();
+        let planets_later = self.states[(self.turn + 1).min(self.states.len() - 1)]
+            .planets
+            .iter();
 
         for (p1, p2) in planets_now.zip(planets_later) {
-            new_vec.push(
-                utils::COLORS[p1.owner.unwrap_or(0) as usize % utils::COLORS.len()].into()
-            );
-            new_vec.push(
-                utils::COLORS[p2.owner.unwrap_or(0) as usize % utils::COLORS.len()].into()
-            );
+            new_vec
+                .push(utils::COLORS[p1.owner.unwrap_or(0) as usize % utils::COLORS.len()].into());
+            new_vec
+                .push(utils::COLORS[p2.owner.unwrap_or(0) as usize % utils::COLORS.len()].into());
         }
 
-        self.current_planet_colours = new_vec;
+        self.current_planet_colours = new_vec.concat::();
     }
 
     fn update_ship_locations(&mut self) {
-        self.ship_locations = Vec::new();
-        self.ship_label_locations = Vec::new();
-        let t = Mat3::new(0.2, 0., 0.,
-                          0., 0.2, 0.0,
-                          0., -0.5, 0.2);
+        let mut new_sl = Vec::new();
+        let mut new_sll = Vec::new();
+
+        let t = Mat3::new(0.2, 0., 0., 0., 0.2, 0.0, 0., -0.5, 0.2);
 
         for ship in self.states[self.turn].expeditions.iter() {
-            let ((o1, a1), (o2, a2)) = self.planet_map.get(&(ship.origin.clone(), ship.destination.clone())).unwrap().get_for_remaining(ship.turns_remaining as usize);
-            self.ship_locations.push((o1 * Mat3::rotate_z(a1)).to_array());
-            self.ship_locations.push((o2 * Mat3::rotate_z(a2)).to_array());
+            let ((o1, a1), (o2, a2)) = self
+                .planet_map
+                .get(&(ship.origin.clone(), ship.destination.clone()))
+                .unwrap()
+                .get_for_remaining(ship.turns_remaining as usize);
+            new_sl.push((o1 * Mat3::rotate_z(a1)).to_array());
+            new_sl.push((o2 * Mat3::rotate_z(a2)).to_array());
 
-            self.ship_label_locations.push((o1 + t).to_array());
-            self.ship_label_locations.push((o2 + t).to_array());
+            new_sll.push((o1 + t).to_array());
+            new_sll.push((o2 + t).to_array());
         }
 
-        self.ship_colours = self.states[self.turn].expeditions.iter().map(|s| {
-            utils::COLORS[s.owner as usize % utils::COLORS.len()].into()
-        }).collect();
+        self.ship_locations = new_sl.concat();
+        self.ship_label_locations = new_sll.concat();
+
+        self.ship_colours = self.states[self.turn]
+            .expeditions
+            .iter()
+            .map(|s| utils::COLORS[s.owner as usize % utils::COLORS.len()])
+            .collect::>()
+            .concat();
     }
 
     fn update_ship_counts(&mut self) {
-        self.ship_counts = self.states[self.turn].expeditions.iter().map(|s| {
-            s.ship_count as usize
-        }).collect();
+        self.ship_counts = self.states[self.turn]
+            .expeditions
+            .iter()
+            .map(|s| s.ship_count as usize)
+            .collect();
     }
 
     pub fn get_max_ships(&self) -> usize {
-        self.states.iter().map(|s| s.expeditions.len()).max().unwrap()
+        self.states
+            .iter()
+            .map(|s| s.expeditions.len())
+            .max()
+            .unwrap()
     }
 
-    pub fn get_ship_count(&self) -> usize {
-        self.states[self.turn].expeditions.len()
+    pub fn get_ship_locations(&self) -> Vec {
+        self.ship_locations.clone()
     }
 
-    pub fn get_ship_locations(&self) -> *const [f32;9] {
-        self.ship_locations.as_ptr()
+    pub fn get_ship_label_locations(&self) -> Vec {
+        self.ship_label_locations.clone()
     }
 
-    pub fn get_ship_label_locations(&self) -> *const [f32;9] {
-        self.ship_label_locations.as_ptr()
+    pub fn get_ship_colours(&self) -> Vec {
+        self.ship_colours.clone()
     }
 
-    pub fn get_ship_colours(&self) -> *const Vec3 {
-        self.ship_colours.as_ptr()
+    pub fn get_ship_counts(&self) -> Vec {
+        self.ship_counts.clone()
     }
 
-    pub fn get_ship_counts(&self) -> *const usize {
-        self.ship_counts.as_ptr()
+    pub fn get_voronoi_verts(&self) -> Vec {
+        self.voronoi_vertices.clone()
     }
 
-    pub fn get_voronoi_vert_count(&self) -> usize {
-        self.voronoi_vertices.len()
+    pub fn get_voronoi_colours(&self) -> Vec {
+        self.voronoi_colors.clone()
     }
 
-    pub fn get_voronoi_verts(&self) -> *const Vec2 {
-        self.voronoi_vertices.as_ptr()
-    }
-
-    pub fn get_voronoi_colours(&self) -> *const Vec3 {
-        self.voronoi_colors.as_ptr()
-    }
-
-    pub fn get_voronoi_ind_count(&self) -> usize {
-        self.voronoi_indices.len()
-    }
-
-    pub fn get_voronoi_inds(&self) -> *const usize {
-        self.voronoi_indices.as_ptr()
+    pub fn get_voronoi_inds(&self) -> Vec {
+        self.voronoi_indices.clone()
     }
 }
 
-
 #[wasm_bindgen]
 extern "C" {
     fn alert(s: &str);
diff --git a/frontend/src/utils.rs b/frontend/src/utils.rs
index 51ecb42..a903912 100644
--- a/frontend/src/utils.rs
+++ b/frontend/src/utils.rs
@@ -9,22 +9,20 @@ pub fn set_panic_hook() {
     console_error_panic_hook::set_once();
 }
 
-use octoon_math::{Vec3};
-
 /// this is total extra, so it the planet viewbox is like 100px wide, it will now be in total 110 pixels wide
 static VIEWBOX_SCALE: f32 = 0.1;
 
 pub static COLORS: [[f32; 3]; 10] = [
-    [0.5 , 0.5 , 0.5  ],
-    [1.0 , 0.50, 0.0  ],    // #FF8000
-    [0.0 , 0.50, 1.0  ],    // #0080ff
-    [1.0 , 0.4 , 0.58 ],    // #FF6693
-    [0.24, 0.79, 0.33 ],    // #3fcb55
-    [0.79, 0.76, 0.24 ],    // #cbc33f
-    [0.81, 0.25, 0.91 ],    // #cf40e9
-    [0.94, 0.32, 0.32 ],    // #FF3F0D
-    [0.11, 0.93, 0.94 ],    // #1beef0
-    [0.05, 0.77, 1.0  ],    // #0DC5FF
+    [0.5, 0.5, 0.5],
+    [1.0, 0.50, 0.0],   // #FF8000
+    [0.0, 0.50, 1.0],   // #0080ff
+    [1.0, 0.4, 0.58],   // #FF6693
+    [0.24, 0.79, 0.33], // #3fcb55
+    [0.79, 0.76, 0.24], // #cbc33f
+    [0.81, 0.25, 0.91], // #cf40e9
+    [0.94, 0.32, 0.32], // #FF3F0D
+    [0.11, 0.93, 0.94], // #1beef0
+    [0.05, 0.77, 1.0],  // #0DC5FF
 ];
 
 use super::types;
@@ -36,16 +34,32 @@ pub fn caclulate_viewbox(planets: &Vec) -> Vec {
         Some(p) => (p.x, p.y, p.x, p.y),
         None => return vec![0.0, 0.0, 0.0, 0.0],
     };
-    let (min_x, min_y, max_x, max_y) = planets
-        .iter()
-        .fold(init, |(min_x, min_y, max_x, max_y), p| (min_x.min(p.x), min_y.min(p.y), max_x.max(p.x), max_y.max(p.y)));
+    let (min_x, min_y, max_x, max_y) =
+        planets
+            .iter()
+            .fold(init, |(min_x, min_y, max_x, max_y), p| {
+                (
+                    min_x.min(p.x),
+                    min_y.min(p.y),
+                    max_x.max(p.x),
+                    max_y.max(p.y),
+                )
+            });
 
     let (width, height) = (max_x - min_x, max_y - min_y);
-    let (dx, dy) = ((VIEWBOX_SCALE * width).max(6.0), (VIEWBOX_SCALE * height).max(6.0));
+    let (dx, dy) = (
+        (VIEWBOX_SCALE * width).max(6.0),
+        (VIEWBOX_SCALE * height).max(6.0),
+    );
 
-    vec![min_x - dx/2.0, min_y - dy/2.0, width + dx, height + dy]
+    vec![min_x - dx / 2.0, min_y - dy / 2.0, width + dx, height + dy]
 }
 
-pub fn get_planets(planets: &Vec, r: f32) -> Vec> {
-    planets.iter().map(|p| Vec3::new(p.x, p.y, r)).collect()
+pub fn get_planets(planets: &Vec, r: f32) -> Vec {
+    planets.iter().fold(Vec::new(), |mut cum, p| {
+        cum.push(p.x);
+        cum.push(p.y);
+        cum.push(r);
+        cum
+    })
 }
diff --git a/frontend/www/src/index.ts b/frontend/www/src/index.ts
index 11e89cf..f824c51 100644
--- a/frontend/www/src/index.ts
+++ b/frontend/www/src/index.ts
@@ -1,7 +1,24 @@
 import { Game } from "planetwars";
 import { memory } from "planetwars/planetwars_bg";
-import { Resizer, resizeCanvasToDisplaySize, FPSCounter, url_to_mesh, Mesh, Dictionary } from "./webgl/util";
-import { Shader, Uniform4f, Uniform3fv, Uniform1f, Uniform2f, ShaderFactory, Uniform3f, UniformMatrix3fv, UniformBool } from './webgl/shader';
+import {
+  Resizer,
+  resizeCanvasToDisplaySize,
+  FPSCounter,
+  url_to_mesh,
+  Mesh,
+  Dictionary,
+} from "./webgl/util";
+import {
+  Shader,
+  Uniform4f,
+  Uniform3fv,
+  Uniform1f,
+  Uniform2f,
+  ShaderFactory,
+  Uniform3f,
+  UniformMatrix3fv,
+  UniformBool,
+} from "./webgl/shader";
 import { Renderer } from "./webgl/renderer";
 import { VertexBuffer, IndexBuffer } from "./webgl/buffer";
 import { VertexBufferLayout, VertexArray } from "./webgl/vertexBufferLayout";
@@ -10,47 +27,57 @@ import { VoronoiBuilder } from "./voronoi/voronoi";
 import { BBox } from "./voronoi/voronoi-core";
 
 function to_bbox(box: number[]): BBox {
-    return {
-        'xl': box[0], 'xr': box[0] + box[2],
-        'yt': box[1], 'yb': box[1] + box[3]
-    };
+  return {
+    xl: box[0],
+    xr: box[0] + box[2],
+    yt: box[1],
+    yb: box[1] + box[3],
+  };
 }
 
 function f32v(ptr: number, size: number): Float32Array {
-    return new Float32Array(memory.buffer, ptr, size);
+  return new Float32Array(memory.buffer, ptr, size);
 }
 
 function i32v(ptr: number, size: number): Int32Array {
-    return new Int32Array(memory.buffer, ptr, size);
+  return new Int32Array(memory.buffer, ptr, size);
 }
 
 export function set_game_name(name: string) {
-    ELEMENTS["name"].innerHTML = name;
+  ELEMENTS["name"].innerHTML = name;
 }
 
 export function set_loading(loading: boolean) {
-    if (loading) {
-        if (!ELEMENTS["main"].classList.contains("loading")) {
-            ELEMENTS["main"].classList.add("loading");
-        }
-    } else {
-        ELEMENTS["main"].classList.remove("loading");
+  if (loading) {
+    if (!ELEMENTS["main"].classList.contains("loading")) {
+      ELEMENTS["main"].classList.add("loading");
     }
+  } else {
+    ELEMENTS["main"].classList.remove("loading");
+  }
 }
 
 const ELEMENTS = {};
-["name", "turnCounter", "main", "turnSlider", "fileselect", "speed", "canvas"].forEach(n => ELEMENTS[n] = document.getElementById(n));
+[
+  "name",
+  "turnCounter",
+  "main",
+  "turnSlider",
+  "fileselect",
+  "speed",
+  "canvas",
+].forEach((n) => (ELEMENTS[n] = document.getElementById(n)));
 
 const CANVAS = ELEMENTS["canvas"];
 const RESOLUTION = [CANVAS.width, CANVAS.height];
 
 const LAYERS = {
-    'vor': -1,  // Background
-    'planet': 1,
-    'planet_label': 2,
-    'ship': 3,
-    'ship_label': 4
-}
+  vor: -1, // Background
+  planet: 1,
+  planet_label: 2,
+  ship: 3,
+  ship_label: 4,
+};
 
 const COUNTER = new FPSCounter();
 
@@ -65,314 +92,427 @@ GL.enable(GL.BLEND);
 GL.blendFunc(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA);
 
 class GameInstance {
-    resizer: Resizer;
-    game: Game;
+  resizer: Resizer;
+  game: Game;
 
-    shader: Shader;
-    vor_shader: Shader;
-    image_shader: Shader;
+  shader: Shader;
+  vor_shader: Shader;
+  image_shader: Shader;
 
-    text_factory: LabelFactory;
-    planet_labels: Label[];
-    ship_labels: Label[];
+  text_factory: LabelFactory;
+  planet_labels: Label[];
+  ship_labels: Label[];
 
-    renderer: Renderer;
-    planet_count: number;
+  renderer: Renderer;
+  planet_count: number;
 
-    vor_builder: VoronoiBuilder;
+  vor_builder: VoronoiBuilder;
 
-    vor_counter = 3;
-    use_vor = true;
-    playing = true;
-    time_stopped_delta = 0;
-    last_time = 0;
-    frame = -1;
+  vor_counter = 3;
+  use_vor = true;
+  playing = true;
+  time_stopped_delta = 0;
+  last_time = 0;
+  frame = -1;
 
-    turn_count = 0;
+  turn_count = 0;
 
-    constructor(game: Game, meshes: Mesh[], ship_mesh: Mesh, shaders: Dictionary) {
-        this.game = game;
-        this.planet_count = this.game.get_planet_count();
+  constructor(
+    game: Game,
+    meshes: Mesh[],
+    ship_mesh: Mesh,
+    shaders: Dictionary
+  ) {
+    this.game = game;
+    const planets = game.get_planets();
+    this.planet_count = planets.length;
 
-        this.shader = shaders["normal"].create_shader(GL, { "MAX_CIRCLES": '' + this.planet_count });
-        this.image_shader = shaders["image"].create_shader(GL);
-        this.vor_shader = shaders["vor"].create_shader(GL, { "PLANETS": '' + this.planet_count });
+    this.shader = shaders["normal"].create_shader(GL, {
+      MAX_CIRCLES: "" + planets.length,
+    });
+    this.image_shader = shaders["image"].create_shader(GL);
+    this.vor_shader = shaders["vor"].create_shader(GL, {
+      PLANETS: "" + planets.length,
+    });
 
-        this.text_factory = defaultLabelFactory(GL, this.image_shader);
-        this.planet_labels = [];
-        this.ship_labels = [];
+    this.text_factory = defaultLabelFactory(GL, this.image_shader);
+    this.planet_labels = [];
+    this.ship_labels = [];
 
-        this.resizer = new Resizer(CANVAS, [...f32v(game.get_viewbox(), 4)], true);
-        this.renderer = new Renderer();
-        this.game.update_turn(0);
+    this.resizer = new Resizer(CANVAS, [...game.get_viewbox()], true);
+    this.renderer = new Renderer();
+    this.game.update_turn(0);
 
-        // Setup key handling
-        document.addEventListener('keydown', this.handleKey.bind(this));
+    // Setup key handling
+    document.addEventListener("keydown", this.handleKey.bind(this));
 
-        // List of [(x, y, r)] for all planets
-        const planets = f32v(game.get_planets(), this.planet_count * 3);
-        this._create_voronoi(planets);
-        this._create_planets(planets, meshes);
-        this._create_shipes(ship_mesh);
+    // List of [(x, y, r)] for all planets
+    this._create_voronoi(planets);
+    this._create_planets(planets, meshes);
+    this._create_shipes(ship_mesh);
 
-        // Set slider correctly
-        this.turn_count = game.turn_count();
-        ELEMENTS["turnSlider"].max = this.turn_count - 1 + '';
+    // Set slider correctly
+    this.turn_count = game.turn_count();
+    ELEMENTS["turnSlider"].max = this.turn_count - 1 + "";
+  }
+
+  _create_voronoi(planets: Float32Array) {
+    const planet_points = [];
+    for (let i = 0; i < planets.length; i += 3) {
+      planet_points.push({ x: -planets[i], y: -planets[i + 1] });
     }
 
-    _create_voronoi(planets: Float32Array) {
-        const planet_points = [];
-        for (let i = 0; i < planets.length; i += 3) {
-            planet_points.push({ 'x': -planets[i], 'y': -planets[i + 1] });
-        }
+    const bbox = to_bbox(this.resizer.get_viewbox());
 
-        const bbox = to_bbox(this.resizer.get_viewbox());
+    this.vor_builder = new VoronoiBuilder(
+      GL,
+      this.vor_shader,
+      planet_points,
+      bbox
+    );
+    this.renderer.addRenderable(this.vor_builder.getRenderable(), LAYERS.vor);
+  }
 
-        this.vor_builder = new VoronoiBuilder(GL, this.vor_shader, planet_points, bbox);
-        this.renderer.addRenderable(this.vor_builder.getRenderable(), LAYERS.vor);
+  _create_planets(planets: Float32Array, meshes: Mesh[]) {
+    for (let i = 0; i < this.planet_count; i++) {
+      {
+        const transform = new UniformMatrix3fv([
+          1,
+          0,
+          0,
+          0,
+          1,
+          0,
+          -planets[i * 3],
+          -planets[i * 3 + 1],
+          1,
+        ]);
+
+        const indexBuffer = new IndexBuffer(
+          GL,
+          meshes[i % meshes.length].cells
+        );
+        const positionBuffer = new VertexBuffer(
+          GL,
+          meshes[i % meshes.length].positions
+        );
+
+        const layout = new VertexBufferLayout();
+        layout.push(GL.FLOAT, 3, 4, "a_position");
+        const vao = new VertexArray();
+        vao.addBuffer(positionBuffer, layout);
+
+        this.renderer.addToDraw(
+          indexBuffer,
+          vao,
+          this.shader,
+          {
+            u_trans: transform,
+            u_trans_next: transform,
+          },
+          [],
+          LAYERS.planet
+        );
+      }
+
+      {
+        const transform = new UniformMatrix3fv([
+          1,
+          0,
+          0,
+          0,
+          1,
+          0,
+          -planets[i * 3],
+          -planets[i * 3 + 1] - 1.2,
+          1,
+        ]);
+
+        const label = this.text_factory.build(GL, transform);
+        this.planet_labels.push(label);
+        this.renderer.addRenderable(label.getRenderable(), LAYERS.planet_label);
+      }
+    }
+  }
+
+  _create_shipes(ship_mesh: Mesh) {
+    const ship_ibo = new IndexBuffer(GL, ship_mesh.cells);
+    const ship_positions = new VertexBuffer(GL, ship_mesh.positions);
+    const ship_layout = new VertexBufferLayout();
+    ship_layout.push(GL.FLOAT, 3, 4, "a_position");
+    const ship_vao = new VertexArray();
+    ship_vao.addBuffer(ship_positions, ship_layout);
+
+    for (let i = 0; i < this.game.get_max_ships(); i++) {
+      this.renderer.addToDraw(
+        ship_ibo,
+        ship_vao,
+        this.shader,
+        {},
+        [],
+        LAYERS.ship
+      );
+
+      const label = this.text_factory.build(GL);
+      this.ship_labels.push(label);
+      this.renderer.addRenderable(label.getRenderable(), LAYERS.ship_label);
+    }
+  }
+
+  on_resize() {
+    this.resizer = new Resizer(CANVAS, [...this.game.get_viewbox()], true);
+    const bbox = to_bbox(this.resizer.get_viewbox());
+    this.vor_builder.resize(GL, bbox);
+  }
+
+  _update_state() {
+    this._update_planets();
+    this._update_ships();
+  }
+
+  _update_planets() {
+    const colours = this.game.get_planet_colors();
+    const planet_ships = this.game.get_planet_ships();
+
+    this.vor_shader.uniform(GL, "u_planet_colours", new Uniform3fv(colours));
+
+    for (let i = 0; i < this.planet_count; i++) {
+      const u = new Uniform3f(
+        colours[i * 6],
+        colours[i * 6 + 1],
+        colours[i * 6 + 2]
+      );
+      this.renderer.updateUniform(
+        i,
+        (us) => (us["u_color"] = u),
+        LAYERS.planet
+      );
+      const u2 = new Uniform3f(
+        colours[i * 6 + 3],
+        colours[i * 6 + 4],
+        colours[i * 6 + 5]
+      );
+      this.renderer.updateUniform(
+        i,
+        (us) => (us["u_color_next"] = u2),
+        LAYERS.planet
+      );
+
+      this.planet_labels[i].setText(
+        GL,
+        "*" + planet_ships[i],
+        Align.Middle,
+        Align.Begin
+      );
+    }
+  }
+
+  _update_ships() {
+    const ships = this.game.get_ship_locations();
+    const labels = this.game.get_ship_label_locations();
+    const ship_counts = this.game.get_ship_counts();
+    const ship_colours = this.game.get_ship_colours();
+
+    for (let i = 0; i < this.game.get_max_ships(); i++) {
+      if (i < ship_counts.length) {
+        this.ship_labels[i].setText(
+          GL,
+          "" + ship_counts[i],
+          Align.Middle,
+          Align.Middle
+        );
+
+        this.renderer.enableRenderable(i, LAYERS.ship);
+        this.renderer.enableRenderable(i, LAYERS.ship_label);
+
+        const u = new Uniform3f(
+          ship_colours[i * 3],
+          ship_colours[i * 3 + 1],
+          ship_colours[i * 3 + 2]
+        );
+
+        const t1 = new UniformMatrix3fv(ships.slice(i * 18, i * 18 + 9));
+        const t2 = new UniformMatrix3fv(ships.slice(i * 18 + 9, i * 18 + 18));
+
+        const tl1 = new UniformMatrix3fv(labels.slice(i * 18, i * 18 + 9));
+        const tl2 = new UniformMatrix3fv(labels.slice(i * 18 + 9, i * 18 + 18));
+
+        this.renderer.updateUniform(
+          i,
+          (us) => {
+            us["u_color"] = u;
+            us["u_color_next"] = u;
+            us["u_trans"] = t1;
+            us["u_trans_next"] = t2;
+          },
+          LAYERS.ship
+        );
+
+        this.renderer.updateUniform(
+          i,
+          (us) => {
+            us["u_trans"] = tl1;
+            us["u_trans_next"] = tl2;
+          },
+          LAYERS.ship_label
+        );
+      } else {
+        this.renderer.disableRenderable(i, LAYERS.ship);
+        this.renderer.disableRenderable(i, LAYERS.ship_label);
+      }
+    }
+  }
+
+  render(time: number) {
+    COUNTER.frame(time);
+
+    if (COUNTER.delta(time) < 30) {
+      this.vor_counter = Math.min(3, this.vor_counter + 1);
+    } else {
+      this.vor_counter = Math.max(-3, this.vor_counter - 1);
     }
 
-    _create_planets(planets: Float32Array, meshes: Mesh[]) {
-        for (let i = 0; i < this.planet_count; i++) {
-            {
-                const transform = new UniformMatrix3fv([
-                    1, 0, 0,
-                    0, 1, 0,
-                    -planets[i * 3], -planets[i * 3 + 1], 1,
-                ]);
-
-                const indexBuffer = new IndexBuffer(GL, meshes[i % meshes.length].cells);
-                const positionBuffer = new VertexBuffer(GL, meshes[i % meshes.length].positions);
-
-                const layout = new VertexBufferLayout();
-                layout.push(GL.FLOAT, 3, 4, "a_position");
-                const vao = new VertexArray();
-                vao.addBuffer(positionBuffer, layout);
-
-                this.renderer.addToDraw(
-                    indexBuffer,
-                    vao,
-                    this.shader,
-                    {
-                        "u_trans": transform,
-                        "u_trans_next": transform,
-                    },
-                    [],
-                    LAYERS.planet
-                );
-            }
-
-            {
-                const transform = new UniformMatrix3fv([
-                    1., 0, 0,
-                    0, 1., 0,
-                    -planets[i * 3], -planets[i * 3 + 1] - 1.2, 1.,
-                ]);
-
-                const label = this.text_factory.build(GL, transform);
-                this.planet_labels.push(label);
-                this.renderer.addRenderable(label.getRenderable(), LAYERS.planet_label);
-            }
-        }
+    if (this.vor_counter < -2) {
+      this.use_vor = false;
     }
 
-    _create_shipes(ship_mesh: Mesh) {
-        const ship_ibo = new IndexBuffer(GL, ship_mesh.cells);
-        const ship_positions = new VertexBuffer(GL, ship_mesh.positions);
-        const ship_layout = new VertexBufferLayout();
-        ship_layout.push(GL.FLOAT, 3, 4, "a_position");
-        const ship_vao = new VertexArray();
-        ship_vao.addBuffer(ship_positions, ship_layout);
+    // If not playing, still reder with different viewbox, so people can still pan etc.
+    if (!this.playing) {
+      this.last_time = time;
 
-        for (let i = 0; i < this.game.get_max_ships(); i++) {
-            this.renderer.addToDraw(
-                ship_ibo,
-                ship_vao,
-                this.shader,
-                {},
-                [],
-                LAYERS.ship
-            );
+      this.shader.uniform(
+        GL,
+        "u_viewbox",
+        new Uniform4f(this.resizer.get_viewbox())
+      );
+      this.vor_shader.uniform(
+        GL,
+        "u_viewbox",
+        new Uniform4f(this.resizer.get_viewbox())
+      );
+      this.image_shader.uniform(
+        GL,
+        "u_viewbox",
+        new Uniform4f(this.resizer.get_viewbox())
+      );
 
-            const label = this.text_factory.build(GL);
-            this.ship_labels.push(label);
-            this.renderer.addRenderable(label.getRenderable(), LAYERS.ship_label)
-        }
+      this.renderer.render(GL);
+      return;
     }
 
-    on_resize() {
-        this.resizer = new Resizer(CANVAS, [...f32v(this.game.get_viewbox(), 4)], true);
-        const bbox = to_bbox(this.resizer.get_viewbox());
-        this.vor_builder.resize(GL, bbox);
+    // Check if turn is still correct
+    if (time > this.last_time + ms_per_frame) {
+      this.last_time = time;
+      this.updateTurn(this.frame + 1);
     }
 
-    _update_state() {
-        this._update_planets();
-        this._update_ships();
+    // Do GL things
+    GL.bindFramebuffer(GL.FRAMEBUFFER, null);
+    GL.viewport(0, 0, GL.canvas.width, GL.canvas.height);
+    GL.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT);
+
+    this.vor_shader.uniform(
+      GL,
+      "u_time",
+      new Uniform1f((time - this.last_time) / ms_per_frame)
+    );
+    this.vor_shader.uniform(
+      GL,
+      "u_viewbox",
+      new Uniform4f(this.resizer.get_viewbox())
+    );
+    this.vor_shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION));
+    this.vor_shader.uniform(GL, "u_vor", new UniformBool(this.use_vor));
+
+    this.shader.uniform(
+      GL,
+      "u_time",
+      new Uniform1f((time - this.last_time) / ms_per_frame)
+    );
+    this.shader.uniform(
+      GL,
+      "u_mouse",
+      new Uniform2f(this.resizer.get_mouse_pos())
+    );
+    this.shader.uniform(
+      GL,
+      "u_viewbox",
+      new Uniform4f(this.resizer.get_viewbox())
+    );
+    this.shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION));
+
+    this.image_shader.uniform(
+      GL,
+      "u_time",
+      new Uniform1f((time - this.last_time) / ms_per_frame)
+    );
+    this.image_shader.uniform(
+      GL,
+      "u_mouse",
+      new Uniform2f(this.resizer.get_mouse_pos())
+    );
+    this.image_shader.uniform(
+      GL,
+      "u_viewbox",
+      new Uniform4f(this.resizer.get_viewbox())
+    );
+    this.image_shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION));
+
+    // Render
+    this.renderer.render(GL);
+
+    COUNTER.frame_end();
+  }
+
+  updateTurn(turn: number) {
+    this.frame = Math.max(0, turn);
+    const new_frame = this.game.update_turn(this.frame);
+    if (new_frame < this.frame) {
+      this.frame = new_frame;
+      this.playing = false;
+    } else {
+      this._update_state();
+      this.playing = true;
     }
 
-    _update_planets() {
-        const colours = f32v(this.game.get_planet_colors(), this.planet_count * 6);
-        const planet_ships = i32v(this.game.get_planet_ships(), this.planet_count);
+    ELEMENTS["turnCounter"].innerHTML =
+      this.frame + " / " + (this.turn_count - 1);
+    ELEMENTS["turnSlider"].value = this.frame + "";
+  }
 
-        this.vor_shader.uniform(GL, "u_planet_colours", new Uniform3fv(colours));
-
-        for (let i = 0; i < this.planet_count; i++) {
-            const u = new Uniform3f(colours[i * 6], colours[i * 6 + 1], colours[i * 6 + 2]);
-            this.renderer.updateUniform(i, (us) => us["u_color"] = u, LAYERS.planet);
-            const u2 = new Uniform3f(colours[i * 6 + 3], colours[i * 6 + 4], colours[i * 6 + 5]);
-            this.renderer.updateUniform(i, (us) => us["u_color_next"] = u2, LAYERS.planet);
-
-            this.planet_labels[i].setText(GL, "*" + planet_ships[i], Align.Middle, Align.Begin);
-        }
+  handleKey(event: KeyboardEvent) {
+    // Space
+    if (event.keyCode == 32) {
+      if (this.playing) {
+        this.playing = false;
+      } else {
+        this.playing = true;
+      }
     }
 
-    _update_ships() {
-        const ship_count = this.game.get_ship_count();
-        const ships = f32v(this.game.get_ship_locations(), ship_count * 9 * 2);
-        const labels = f32v(this.game.get_ship_label_locations(), ship_count * 9 * 2);
-        const ship_counts = i32v(this.game.get_ship_counts(), ship_count);
-        const ship_colours = f32v(this.game.get_ship_colours(), ship_count * 3);
-
-        for (let i = 0; i < this.game.get_max_ships(); i++) {
-            if (i < ship_count) {
-                this.ship_labels[i].setText(GL, "" + ship_counts[i], Align.Middle, Align.Middle);
-
-                this.renderer.enableRenderable(i, LAYERS.ship);
-                this.renderer.enableRenderable(i, LAYERS.ship_label);
-
-                const u = new Uniform3f(ship_colours[i * 3], ship_colours[i * 3 + 1], ship_colours[i * 3 + 2]);
-
-                const t1 = new UniformMatrix3fv(ships.slice(i * 18, i * 18 + 9));
-                const t2 = new UniformMatrix3fv(ships.slice(i * 18 + 9, i * 18 + 18));
-
-                const tl1 = new UniformMatrix3fv(labels.slice(i * 18, i * 18 + 9));
-                const tl2 = new UniformMatrix3fv(labels.slice(i * 18 + 9, i * 18 + 18));
-
-                this.renderer.updateUniform(i, (us) => {
-                    us["u_color"] = u;
-                    us["u_color_next"] = u;
-                    us["u_trans"] = t1;
-                    us["u_trans_next"] = t2;
-                }, LAYERS.ship);
-
-                this.renderer.updateUniform(i, (us) => {
-                    us["u_trans"] = tl1;
-                    us["u_trans_next"] = tl2;
-                }, LAYERS.ship_label);
-
-            } else {
-                this.renderer.disableRenderable(i, LAYERS.ship);
-                this.renderer.disableRenderable(i, LAYERS.ship_label);
-            }
-        }
+    // Arrow left
+    if (event.keyCode == 37) {
+      // This feels more natural than -1 what it should be, I think
+      this.updateTurn(this.frame - 2);
     }
 
-    render(time: number) {
-        COUNTER.frame(time);
-
-        if (COUNTER.delta(time) < 30) {
-            this.vor_counter = Math.min(3, this.vor_counter + 1);
-        } else {
-            this.vor_counter = Math.max(-3, this.vor_counter - 1);
-        }
-
-        if (this.vor_counter < -2) {
-            this.use_vor = false;
-        }
-
-        // If not playing, still reder with different viewbox, so people can still pan etc.
-        if (!this.playing) {
-            this.last_time = time;
-
-            this.shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox()));
-            this.vor_shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox()));
-            this.image_shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox()));
-
-            this.renderer.render(GL);
-            return;
-        }
-
-        // Check if turn is still correct
-        if (time > this.last_time + ms_per_frame) {
-            this.last_time = time;
-            this.updateTurn(this.frame + 1);
-        }
-
-        // Do GL things
-        GL.bindFramebuffer(GL.FRAMEBUFFER, null);
-        GL.viewport(0, 0, GL.canvas.width, GL.canvas.height);
-        GL.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT);
-
-        this.vor_shader.uniform(GL, "u_time", new Uniform1f((time - this.last_time) / ms_per_frame));
-        this.vor_shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox()));
-        this.vor_shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION));
-        this.vor_shader.uniform(GL, "u_vor", new UniformBool(this.use_vor));
-
-        this.shader.uniform(GL, "u_time", new Uniform1f((time - this.last_time) / ms_per_frame));
-        this.shader.uniform(GL, "u_mouse", new Uniform2f(this.resizer.get_mouse_pos()));
-        this.shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox()));
-        this.shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION));
-
-        this.image_shader.uniform(GL, "u_time", new Uniform1f((time - this.last_time) / ms_per_frame));
-        this.image_shader.uniform(GL, "u_mouse", new Uniform2f(this.resizer.get_mouse_pos()));
-        this.image_shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox()));
-        this.image_shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION));
-
-        // Render
-        this.renderer.render(GL);
-
-        COUNTER.frame_end();
+    // Arrow right
+    if (event.keyCode == 39) {
+      this.updateTurn(this.frame + 1);
     }
 
-    updateTurn(turn: number) {
-        this.frame = Math.max(0, turn);
-        const new_frame = this.game.update_turn(this.frame);
-        if (new_frame < this.frame) {
-            this.frame = new_frame;
-            this.playing = false;
-        } else {
-            this._update_state();
-            this.playing = true;
-        }
-
-        ELEMENTS["turnCounter"].innerHTML = this.frame + " / " + (this.turn_count - 1);
-        ELEMENTS["turnSlider"].value = this.frame + '';
+    // d key
+    if (event.keyCode == 68) {
+      ELEMENTS["speed"].value = ms_per_frame + 10 + "";
+      ELEMENTS["speed"].onchange(undefined);
     }
 
-    handleKey(event: KeyboardEvent) {
-        // Space
-        if (event.keyCode == 32) {
-            if (this.playing) {
-                this.playing = false;
-            } else {
-                this.playing = true;
-            }
-        }
-
-        // Arrow left
-        if (event.keyCode == 37) {
-            // This feels more natural than -1 what it should be, I think
-            this.updateTurn(this.frame - 2);
-        }
-
-        // Arrow right
-        if (event.keyCode == 39) {
-            this.updateTurn(this.frame + 1);
-        }
-
-        // d key
-        if (event.keyCode == 68) {
-            ELEMENTS["speed"].value = ms_per_frame + 10 + '';
-            ELEMENTS["speed"].onchange(undefined);
-        }
-
-        // a key
-        if (event.keyCode == 65) {
-            ELEMENTS["speed"].value = Math.max(ms_per_frame - 10, 0) + '';
-            ELEMENTS["speed"].onchange(undefined);
-        }
+    // a key
+    if (event.keyCode == 65) {
+      ELEMENTS["speed"].value = Math.max(ms_per_frame - 10, 0) + "";
+      ELEMENTS["speed"].onchange(undefined);
     }
+  }
 }
 
 var game_instance: GameInstance;
@@ -380,59 +520,87 @@ var meshes: Mesh[];
 var shaders: Dictionary;
 
 export async function set_instance(source: string) {
-    if (!meshes || !shaders) {
-        const mesh_promises = ["ship.svg", "earth.svg", "mars.svg", "venus.svg"].map(
-            (name) => "static/res/assets/" + name
-        ).map(url_to_mesh);
+  if (!meshes || !shaders) {
+    const mesh_promises = ["ship.svg", "earth.svg", "mars.svg", "venus.svg"]
+      .map((name) => "static/res/assets/" + name)
+      .map(url_to_mesh);
 
-        const shader_promies = [
-            (async () => <[string, ShaderFactory]>["normal", await ShaderFactory.create_factory("static/shaders/frag/simple.glsl", "static/shaders/vert/simple.glsl")])(),
-            (async () => <[string, ShaderFactory]>["vor", await ShaderFactory.create_factory("static/shaders/frag/vor.glsl", "static/shaders/vert/vor.glsl")])(),
-            (async () => <[string, ShaderFactory]>["image", await ShaderFactory.create_factory("static/shaders/frag/image.glsl", "static/shaders/vert/simple.glsl")])(),
-        ];
-        let shaders_array: [string, ShaderFactory][];
-        [meshes, shaders_array] = await Promise.all(
-            [
-                Promise.all(mesh_promises),
-                Promise.all(shader_promies),
-            ]
-        );
+    const shader_promies = [
+      (async () =>
+        <[string, ShaderFactory]>[
+          "normal",
+          await ShaderFactory.create_factory(
+            "static/shaders/frag/simple.glsl",
+            "static/shaders/vert/simple.glsl"
+          ),
+        ])(),
+      (async () =>
+        <[string, ShaderFactory]>[
+          "vor",
+          await ShaderFactory.create_factory(
+            "static/shaders/frag/vor.glsl",
+            "static/shaders/vert/vor.glsl"
+          ),
+        ])(),
+      (async () =>
+        <[string, ShaderFactory]>[
+          "image",
+          await ShaderFactory.create_factory(
+            "static/shaders/frag/image.glsl",
+            "static/shaders/vert/simple.glsl"
+          ),
+        ])(),
+    ];
+    let shaders_array: [string, ShaderFactory][];
+    [meshes, shaders_array] = await Promise.all([
+      Promise.all(mesh_promises),
+      Promise.all(shader_promies),
+    ]);
 
-        shaders = {};
-        shaders_array.forEach(([name, fac]) => shaders[name] = fac);
-    }
+    shaders = {};
+    shaders_array.forEach(([name, fac]) => (shaders[name] = fac));
+  }
 
-    resizeCanvasToDisplaySize(CANVAS);
+  resizeCanvasToDisplaySize(CANVAS);
 
-    game_instance = new GameInstance(Game.new(source), meshes.slice(1), meshes[0], shaders);
+  game_instance = new GameInstance(
+    Game.new(source),
+    meshes.slice(1),
+    meshes[0],
+    shaders
+  );
 
-    set_loading(false);
+  set_loading(false);
 }
 
-window.addEventListener('resize', function () {
+window.addEventListener(
+  "resize",
+  function () {
     resizeCanvasToDisplaySize(CANVAS);
 
     if (game_instance) {
-        game_instance.on_resize();
+      game_instance.on_resize();
     }
-}, { capture: false, passive: true })
+  },
+  { capture: false, passive: true }
+);
 
 ELEMENTS["turnSlider"].oninput = function () {
-    if (game_instance) {
-        game_instance.updateTurn(parseInt(ELEMENTS["turnSlider"].value));
-    }
-}
+  if (game_instance) {
+    game_instance.updateTurn(parseInt(ELEMENTS["turnSlider"].value));
+  }
+};
 
 ELEMENTS["speed"].onchange = function () {
-    ms_per_frame = parseInt(ELEMENTS["speed"].value);
-}
+  ms_per_frame = parseInt(ELEMENTS["speed"].value);
+};
 
 function step(time: number) {
-    if (game_instance) {
-        game_instance.render(time);
-    }
+  if (game_instance) {
+    game_instance.render(time);
+  }
 
-    requestAnimationFrame(step);
+  requestAnimationFrame(step);
 }
 
 requestAnimationFrame(step);