From 9c696fa89b15cf30e09ac5887cbf7ba06f5653d3 Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Sat, 14 Sep 2019 14:35:15 +0200 Subject: [PATCH 1/8] bump --- backend/src/planetwars/mod.rs | 0 backend/src/pw/mod.rs | 11 + backend/src/pw/pw_config.rs | 79 ++++++ backend/src/pw/pw_controller.rs | 452 ++++++++++++++++++++++++++++++++ backend/src/pw/pw_protocol.rs | 105 ++++++++ backend/src/pw/pw_rules.rs | 200 ++++++++++++++ backend/src/pw/pw_serializer.rs | 61 +++++ 7 files changed, 908 insertions(+) create mode 100644 backend/src/planetwars/mod.rs create mode 100644 backend/src/pw/mod.rs create mode 100644 backend/src/pw/pw_config.rs create mode 100644 backend/src/pw/pw_controller.rs create mode 100644 backend/src/pw/pw_protocol.rs create mode 100644 backend/src/pw/pw_rules.rs create mode 100644 backend/src/pw/pw_serializer.rs diff --git a/backend/src/planetwars/mod.rs b/backend/src/planetwars/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/pw/mod.rs b/backend/src/pw/mod.rs new file mode 100644 index 0000000..9e1ade5 --- /dev/null +++ b/backend/src/pw/mod.rs @@ -0,0 +1,11 @@ + +pub mod pw_protocol; +mod pw_controller; +mod pw_rules; +mod pw_config; +mod pw_serializer; + +pub use self::pw_controller::PwMatch; +pub use self::pw_rules::PlanetWars; +pub use self::pw_config::Config; +pub use self::pw_protocol::Map; diff --git a/backend/src/pw/pw_config.rs b/backend/src/pw/pw_config.rs new file mode 100644 index 0000000..694d0f4 --- /dev/null +++ b/backend/src/pw/pw_config.rs @@ -0,0 +1,79 @@ +use std::fs::File; +use std::io::Read; +use std::io; + +use serde_json; + +use super::pw_protocol as proto; +use super::pw_rules::*; + +// TODO +use server::ClientId; + + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub map_file: String, + pub max_turns: u64, +} + +impl Config { + pub fn create_game(&self, clients: Vec) -> PlanetWars { + let planets = self.load_map(clients.len()); + let players = clients.into_iter() + .map(|client_id| Player { id: client_id, alive: true }) + .collect(); + + PlanetWars { + players: players, + planets: planets, + expeditions: Vec::new(), + expedition_num: 0, + turn_num: 0, + max_turns: self.max_turns, + } + } + + fn load_map(&self, num_players: usize) -> Vec { + let map = self.read_map().expect("[PLANET_WARS] reading map failed"); + + return map.planets + .into_iter() + .enumerate() + .map(|(num, planet)| { + let mut fleets = Vec::new(); + let owner = planet.owner.and_then(|owner_num| { + // in the current map format, player numbers start at 1. + // TODO: we might want to change this. + let player_num = owner_num as usize - 1; + // ignore players that are not in the game + if player_num < num_players { + Some(player_num) + } else { + None + } + }); + if planet.ship_count > 0 { + fleets.push(Fleet { + owner: owner, + ship_count: planet.ship_count, + }); + } + return Planet { + id: num, + name: planet.name, + x: planet.x, + y: planet.y, + fleets: fleets, + }; + }).collect(); + } + + fn read_map(&self) -> io::Result { + let mut file = File::open(&self.map_file)?; + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + let map = serde_json::from_str(&buf)?; + return Ok(map); + } +} diff --git a/backend/src/pw/pw_controller.rs b/backend/src/pw/pw_controller.rs new file mode 100644 index 0000000..1a7ac5f --- /dev/null +++ b/backend/src/pw/pw_controller.rs @@ -0,0 +1,452 @@ +use std::collections::{HashMap, HashSet}; +use std::time::{Duration, Instant}; +use std::mem; +use std::io; + +use events; +use network::server::RegisteredHandle; +use reactors::reactor::ReactorHandle; +use reactors::{WireEvent, RequestHandler}; +use server::{ClientId, ConnectionManager}; +use sodiumoxide::crypto::sign::PublicKey; + +use super::Config; +use super::pw_rules::{PlanetWars, Dispatch}; +use super::pw_serializer::serialize_state; +use super::pw_protocol::{ + self as proto, + PlayerAction, + PlayerCommand, + CommandError, +}; + +use serde_json; + +pub struct Player { + id: ClientId, + num: usize, + handle: RegisteredHandle, +} + +pub struct ClientHandler { + client_id: u32, + reactor_handle: ReactorHandle, +} + +impl ClientHandler { + pub fn new(client_id: u32, reactor_handle: ReactorHandle) -> Self { + ClientHandler { + client_id, + reactor_handle, + } + } + + pub fn on_connect(&mut self, _event: &events::Connected) + -> io::Result + { + self.reactor_handle.dispatch(events::ClientConnected { + client_id: self.client_id, + }); + Ok(WireEvent::null()) + } + + pub fn on_disconnect(&mut self, _event: &events::Disconnected) + -> io::Result + { + self.reactor_handle.dispatch(events::ClientDisconnected { + client_id: self.client_id, + }); + Ok(WireEvent::null()) + } + + pub fn on_message(&mut self, event: &events::ClientSend) + -> io::Result + { + self.reactor_handle.dispatch(events::ClientMessage { + client_id: self.client_id, + data: event.data.clone(), + }); + Ok(WireEvent::null()) + } +} + +pub struct PwMatch { + state: PwMatchState, +} + +enum PwMatchState { + Lobby(Lobby), + Playing(PwController), + Finished, +} + +impl PwMatch { + pub fn new(match_uuid: Vec, + reactor_handle: ReactorHandle, + connection_manager: ConnectionManager) + -> Self + { + let lobby = Lobby::new( + match_uuid, + connection_manager, + reactor_handle + ); + + return PwMatch { + state: PwMatchState::Lobby(lobby), + } + } + + fn take_state(&mut self) -> PwMatchState { + mem::replace(&mut self.state, PwMatchState::Finished) + } + + pub fn register_client(&mut self, event: &events::RegisterClient) { + if let &mut PwMatchState::Lobby(ref mut lobby) = &mut self.state { + let client_id = ClientId::new(event.client_id); + let key = PublicKey::from_slice(&event.public_key).unwrap(); + lobby.add_player(client_id, key); + } + } + + pub fn remove_client(&mut self, event: &events::RemoveClient) { + if let PwMatchState::Lobby(ref mut lobby) = self.state { + let client_id = ClientId::new(event.client_id); + lobby.remove_player(client_id); + } + } + + pub fn start_game(&mut self, event: &events::StartGame) { + let state = self.take_state(); + + if let PwMatchState::Lobby(lobby) = state { + let config = Config { + map_file: event.map_path.clone(), + max_turns: event.max_turns as u64, + }; + self.state = PwMatchState::Playing(PwController::new( + config, + lobby.reactor_handle, + lobby.connection_manager, + lobby.players, + )); + } else { + self.state = state; + } + } + + pub fn game_step(&mut self, event: &events::GameStep) { + if let PwMatchState::Playing(ref mut controller) = self.state { + controller.on_step(event); + } + } + + pub fn client_message(&mut self, event: &events::ClientMessage) { + if let PwMatchState::Playing(ref mut controller) = self.state { + controller.on_client_message(event); + } + } + + pub fn game_finished(&mut self, event: &events::GameFinished) { + let state = self.take_state(); + if let PwMatchState::Playing(mut controller) = state { + controller.on_finished(event); + } + self.state = PwMatchState::Finished; + } + + pub fn timeout(&mut self, event: &events::TurnTimeout) { + if let PwMatchState::Playing(ref mut controller) = self.state { + controller.on_timeout(event); + } + } +} + +pub struct Lobby { + match_uuid: Vec, + connection_manager: ConnectionManager, + reactor_handle: ReactorHandle, + + players: HashMap, +} + +impl Lobby { + fn new(match_uuid: Vec, + connection_manager: ConnectionManager, + reactor_handle: ReactorHandle) + -> Self + { + return Lobby { + match_uuid, + connection_manager, + reactor_handle, + + players: HashMap::new(), + // start counter at 1, because 0 is the control client + } + } + + fn add_player(&mut self, client_id: ClientId, public_key: PublicKey) { + let mut core = RequestHandler::new( + ClientHandler::new( + client_id.as_u32(), + self.reactor_handle.clone(), + ), + ); + + core.add_handler(ClientHandler::on_connect); + core.add_handler(ClientHandler::on_disconnect); + core.add_handler(ClientHandler::on_message); + + let handle = self.connection_manager.create_client( + self.match_uuid.clone(), + client_id.as_u32(), + public_key, + core + ); + + self.players.insert(client_id, handle); + } + + fn remove_player(&mut self, client_id: ClientId) { + self.players.remove(&client_id); + } +} + +pub struct PwController { + state: PlanetWars, + planet_map: HashMap, + reactor_handle: ReactorHandle, + connection_manager: ConnectionManager, + + client_player: HashMap, + players: HashMap, + + waiting_for: HashSet, + commands: HashMap, +} + +impl PwController { + pub fn new(config: Config, + reactor_handle: ReactorHandle, + connection_manager: ConnectionManager, + clients: HashMap) + -> Self + { + // TODO: we probably want a way to fixate player order + let client_ids = clients.keys().cloned().collect(); + let state = config.create_game(client_ids); + + let planet_map = state.planets.iter().map(|planet| { + (planet.name.clone(), planet.id) + }).collect(); + + let mut client_player = HashMap::new(); + let mut players = HashMap::new(); + + let clients_iter = clients.into_iter().enumerate(); + for (player_num, (client_id, client_handle)) in clients_iter { + client_player.insert(client_id, player_num); + players.insert(client_id, Player { + id: client_id, + num: player_num, + handle: client_handle, + }); + } + + let mut controller = PwController { + state, + planet_map, + players, + client_player, + reactor_handle, + connection_manager, + + waiting_for: HashSet::new(), + commands: HashMap::new(), + }; + // this initial dispatch starts the game + controller.dispatch_state(); + return controller; + } + + + /// Advance the game by one turn. + fn step(&mut self) { + self.state.repopulate(); + self.execute_commands(); + self.state.step(); + + self.dispatch_state(); + } + + fn dispatch_state(&mut self) { + let turn_num = self.state.turn_num; + // TODO: fix this + let state = serde_json::to_string(&serialize_state(&self.state)).unwrap(); + + if self.state.is_finished() { + let event = events::GameFinished { turn_num, state }; + self.reactor_handle.dispatch(event); + } else { + let event = events::GameStep { turn_num, state }; + self.reactor_handle.dispatch(event); + } + } + + fn on_step(&mut self, step: &events::GameStep) { + let state = &self.state; + let waiting_for = &mut self.waiting_for; + + self.players.retain(|_, player| { + if state.players[player.num].alive { + waiting_for.insert(player.id); + player.handle.dispatch(events::GameStep { + turn_num: step.turn_num, + state: step.state.clone(), + }); + // keep this player in the game + return true; + } else { + player.handle.dispatch(events::GameFinished { + turn_num: step.turn_num, + state: step.state.clone(), + }); + // this player is dead, kick him! + // TODO: shutdown the reactor + // TODO: todo + return false; + } + }); + + let deadline = Instant::now() + Duration::from_secs(1); + self.reactor_handle.dispatch_at(deadline, events::TurnTimeout { + turn_num: state.turn_num, + }); + } + + fn on_client_message(&mut self, event: &events::ClientMessage) { + let client_id = ClientId::new(event.client_id); + self.waiting_for.remove(&client_id); + self.commands.insert(client_id, event.data.clone()); + + if self.waiting_for.is_empty() { + self.step(); + } + } + + fn on_finished(&mut self, event: &events::GameFinished) { + self.players.retain(|_player_id, player| { + player.handle.dispatch(events::GameFinished { + turn_num: event.turn_num, + state: event.state.clone(), + }); + // game is over, kick everyone. + false + }); + println!("everybody has been kicked"); + self.reactor_handle.quit(); + } + + fn on_timeout(&mut self, event: &events::TurnTimeout) { + if self.state.turn_num == event.turn_num { + self.step(); + } + } + + fn player_commands(&mut self) -> HashMap> { + let commands = &mut self.commands; + return self.players.values().map(|player| { + let command = commands.remove(&player.id); + return (player.id, command); + }).collect(); + } + + fn execute_commands(&mut self) { + let mut commands = self.player_commands(); + + for (player_id, command) in commands.drain() { + let player_num = self.players[&player_id].num; + let action = self.execute_action(player_num, command); + let serialized_action = serde_json::to_string(&action).unwrap(); + self.reactor_handle.dispatch(events::PlayerAction { + client_id: player_id.as_u32(), + action: serialized_action.clone(), + }); + self.players + .get_mut(&player_id) + .unwrap() + .handle + .dispatch(events::PlayerAction { + client_id: player_id.as_u32(), + action: serialized_action, + }); + } + } + + fn execute_action(&mut self, player_num: usize, response: Option) + -> PlayerAction + { + // TODO: it would be cool if this could be done with error_chain. + + let message = match response { + None => return PlayerAction::Timeout, + Some(message) => message, + }; + + let action: proto::Action = match serde_json::from_str(&message) { + Err(err) => return PlayerAction::ParseError(err.to_string()), + Ok(action) => action, + }; + + let commands = action.commands.into_iter().map(|command| { + match self.parse_command(player_num, &command) { + Ok(dispatch) => { + self.state.dispatch(&dispatch); + PlayerCommand { + command, + error: None, + } + }, + Err(error) => { + PlayerCommand { + command, + error: Some(error), + } + } + } + }).collect(); + + return PlayerAction::Commands(commands); + } + + fn parse_command(&self, player_num: usize, mv: &proto::Command) + -> Result + { + let origin_id = *self.planet_map + .get(&mv.origin) + .ok_or(CommandError::OriginDoesNotExist)?; + + let target_id = *self.planet_map + .get(&mv.destination) + .ok_or(CommandError::DestinationDoesNotExist)?; + + if self.state.planets[origin_id].owner() != Some(player_num) { + return Err(CommandError::OriginNotOwned); + } + + if self.state.planets[origin_id].ship_count() < mv.ship_count { + return Err(CommandError::NotEnoughShips); + } + + if mv.ship_count == 0 { + return Err(CommandError::ZeroShipMove); + } + + Ok(Dispatch { + origin: origin_id, + target: target_id, + ship_count: mv.ship_count, + }) + } +} \ No newline at end of file diff --git a/backend/src/pw/pw_protocol.rs b/backend/src/pw/pw_protocol.rs new file mode 100644 index 0000000..9eb5c1c --- /dev/null +++ b/backend/src/pw/pw_protocol.rs @@ -0,0 +1,105 @@ +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Expedition { + pub id: u64, + pub ship_count: u64, + pub origin: String, + pub destination: String, + pub owner: u32, + pub turns_remaining: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Planet { + pub ship_count: u64, + pub x: f64, + pub y: f64, + pub owner: Option, + pub name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Action { + #[serde(rename = "moves")] + pub commands: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Command { + pub origin: String, + pub destination: String, + pub ship_count: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Map { + pub planets: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct State { + pub planets: Vec, + pub expeditions: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GameInfo { + pub players: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CommandError { + NotEnoughShips, + OriginNotOwned, + ZeroShipMove, + OriginDoesNotExist, + DestinationDoesNotExist, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlayerCommand { + pub command: Command, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type", content = "value")] +pub enum PlayerAction { + Timeout, + ParseError(String), + Commands(Vec), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type", content = "content")] +pub enum ServerMessage { + /// Game state in current turn + GameState(State), + /// The action that was performed + PlayerAction(PlayerAction), + /// The game is over, and this is the concluding state. + FinalState(State), +} + +// lobby messages +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type", content = "content")] +pub enum ControlMessage { + PlayerConnected { + player_id: u64, + }, + PlayerDisconnected { + player_id: u64, + }, + GameState(State), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type", content = "content")] +pub enum LobbyCommand { + StartMatch, +} diff --git a/backend/src/pw/pw_rules.rs b/backend/src/pw/pw_rules.rs new file mode 100644 index 0000000..ed66b26 --- /dev/null +++ b/backend/src/pw/pw_rules.rs @@ -0,0 +1,200 @@ +use server::ClientId; + +/// The planet wars game rules. +pub struct PlanetWars { + pub players: Vec, + pub planets: Vec, + pub expeditions: Vec, + // How many expeditions were already dispatched. + // This is needed for assigning expedition identifiers. + pub expedition_num: u64, + pub turn_num: u64, + pub max_turns: u64, +} + +#[derive(Debug)] +pub struct Player { + pub id: ClientId, + pub alive: bool, +} + +#[derive(Debug)] +pub struct Fleet { + pub owner: Option, + pub ship_count: u64, +} + +#[derive(Debug)] +pub struct Planet { + pub id: usize, + pub name: String, + pub fleets: Vec, + pub x: f64, + pub y: f64, +} + +#[derive(Debug)] +pub struct Expedition { + pub id: u64, + pub origin: usize, + pub target: usize, + pub fleet: Fleet, + pub turns_remaining: u64, +} + +#[derive(Debug)] +pub struct Dispatch { + pub origin: usize, + pub target: usize, + pub ship_count: u64, +} + +impl PlanetWars { + + pub fn dispatch(&mut self, dispatch: &Dispatch) { + let distance = self.planets[dispatch.origin].distance( + &self.planets[dispatch.target] + ); + + let origin = &mut self.planets[dispatch.origin]; + origin.fleets[0].ship_count -= dispatch.ship_count; + + + let expedition = Expedition { + id: self.expedition_num, + origin: dispatch.origin, + target: dispatch.target, + turns_remaining: distance, + fleet: Fleet { + owner: origin.owner(), + ship_count: dispatch.ship_count, + }, + }; + + // increment counter + self.expedition_num += 1; + self.expeditions.push(expedition); + } + + // Play one step of the game + pub fn step(&mut self) { + self.turn_num += 1; + + // Initially mark all players dead, re-marking them as alive once we + // encounter a sign of life. + for player in self.players.iter_mut() { + player.alive = false; + } + + self.step_expeditions(); + self.resolve_combat(); + } + + pub fn repopulate(&mut self) { + for planet in self.planets.iter_mut() { + if planet.owner().is_some() { + planet.fleets[0].ship_count += 1; + } + } + } + + fn step_expeditions(&mut self) { + let mut i = 0; + let exps = &mut self.expeditions; + while i < exps.len() { + // compare with 1 to avoid issues with planet distance 0 + if exps[i].turns_remaining <= 1 { + // remove expedition from expeditions, and add to fleet + let exp = exps.swap_remove(i); + let planet = &mut self.planets[exp.target]; + planet.orbit(exp.fleet); + } else { + exps[i].turns_remaining -= 1; + if let Some(owner_num) = exps[i].fleet.owner { + // owner has an expedition in progress; this is a sign of life. + self.players[owner_num].alive = true; + } + + // proceed to next expedition + i += 1; + } + } + } + + fn resolve_combat(&mut self) { + for planet in self.planets.iter_mut() { + planet.resolve_combat(); + if let Some(owner_num) = planet.owner() { + // owner owns a planet; this is a sign of life. + self.players[owner_num].alive = true; + } + } + } + + pub fn is_finished(&self) -> bool { + let remaining = self.players.iter().filter(|p| p.alive).count(); + return remaining < 2 || self.turn_num >= self.max_turns; + } + + pub fn living_players(&self) -> Vec { + self.players.iter().filter_map(|p| { + if p.alive { + Some(p.id) + } else { + None + } + }).collect() + } +} + + +impl Planet { + pub fn owner(&self) -> Option { + self.fleets.first().and_then(|f| f.owner) + } + + pub fn ship_count(&self) -> u64 { + self.fleets.first().map_or(0, |f| f.ship_count) + } + + /// Make a fleet orbit this planet. + fn orbit(&mut self, fleet: Fleet) { + // If owner already has a fleet present, merge + for other in self.fleets.iter_mut() { + if other.owner == fleet.owner { + other.ship_count += fleet.ship_count; + return; + } + } + // else, add fleet to fleets list + self.fleets.push(fleet); + } + + fn resolve_combat(&mut self) { + // The player owning the largest fleet present will win the combat. + // Here, we resolve how many ships he will have left. + // note: in the current implementation, we could resolve by doing + // winner.ship_count -= second_largest.ship_count, but this does not + // allow for simple customizations (such as changing combat balance). + + self.fleets.sort_by(|a, b| a.ship_count.cmp(&b.ship_count).reverse()); + while self.fleets.len() > 1 { + let fleet = self.fleets.pop().unwrap(); + // destroy some ships + for other in self.fleets.iter_mut() { + other.ship_count -= fleet.ship_count; + } + + // remove dead fleets + while self.fleets.last().map(|f| f.ship_count) == Some(0) { + self.fleets.pop(); + } + } + } + + fn distance(&self, other: &Planet) -> u64 { + let dx = self.x - other.x; + let dy = self.y - other.y; + return (dx.powi(2) + dy.powi(2)).sqrt().ceil() as u64; + } +} \ No newline at end of file diff --git a/backend/src/pw/pw_serializer.rs b/backend/src/pw/pw_serializer.rs new file mode 100644 index 0000000..c258f7e --- /dev/null +++ b/backend/src/pw/pw_serializer.rs @@ -0,0 +1,61 @@ +use super::pw_rules::{PlanetWars, Planet, Expedition}; +use super::pw_protocol as proto; + +/// Serialize given gamestate +pub fn serialize_state(state: &PlanetWars) -> proto::State { + let serializer = Serializer::new(state); + serializer.serialize_state() +} + +struct Serializer<'a> { + state: &'a PlanetWars, +} + +impl<'a> Serializer<'a> { + fn new(state: &'a PlanetWars) -> Self { + Serializer { + state: state, + } + } + + fn serialize_state(&self) -> proto::State { + proto::State { + planets: self.state + .planets + .iter() + .map(|planet| self.serialize_planet(planet)) + .collect(), + expeditions: self.state + .expeditions + .iter() + .map(|exp| self.serialize_expedition(exp)) + .collect(), + } + } + + // gets the client id for a player number + fn player_client_id(&self, player_num: usize) -> u32 { + self.state.players[player_num].id.as_u32() + } + + fn serialize_planet(&self, planet: &Planet) -> proto::Planet { + proto::Planet { + name: planet.name.clone(), + x: planet.x, + y: planet.y, + owner: planet.owner().map(|num| self.player_client_id(num)), + ship_count: planet.ship_count(), + } + } + + fn serialize_expedition(&self, exp: &Expedition) -> proto::Expedition { + proto::Expedition { + id: exp.id, + owner: self.player_client_id(exp.fleet.owner.unwrap()), + ship_count: exp.fleet.ship_count, + origin: self.state.planets[exp.origin].name.clone(), + destination: self.state.planets[exp.target].name.clone(), + turns_remaining: exp.turns_remaining, + } + } +} From 0e9e36c0c8ba0b454f4382986d50d490d1e5c98b Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Sat, 14 Sep 2019 18:15:19 +0200 Subject: [PATCH 2/8] implement planetwars, for the most part --- backend/Cargo.toml | 3 + backend/src/main.rs | 7 + backend/src/planetwars/mod.rs | 124 +++++ backend/src/{pw => planetwars}/pw_config.rs | 15 +- backend/src/{pw => planetwars}/pw_protocol.rs | 30 +- backend/src/{pw => planetwars}/pw_rules.rs | 7 +- .../src/{pw => planetwars}/pw_serializer.rs | 33 +- backend/src/pw/mod.rs | 11 - backend/src/pw/pw_controller.rs | 452 ------------------ 9 files changed, 170 insertions(+), 512 deletions(-) rename backend/src/{pw => planetwars}/pw_config.rs (88%) rename backend/src/{pw => planetwars}/pw_protocol.rs (74%) rename backend/src/{pw => planetwars}/pw_rules.rs (98%) rename backend/src/{pw => planetwars}/pw_serializer.rs (51%) delete mode 100644 backend/src/pw/mod.rs delete mode 100644 backend/src/pw/pw_controller.rs diff --git a/backend/Cargo.toml b/backend/Cargo.toml index b9d6f95..1eaa665 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -11,3 +11,6 @@ mozaic = { git = "https://github.com/ajuvercr/MOZAIC" } tokio = "0.1.22" rand = { version = "0.6.5", default-features = true } futures = "0.1.28" +serde = "1.0.100" +serde_derive = "1.0.100" +serde_json = "1.0" diff --git a/backend/src/main.rs b/backend/src/main.rs index 23e0956..c2f7495 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,3 +1,7 @@ +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; extern crate tokio; extern crate futures; @@ -11,6 +15,9 @@ use mozaic::errors; use mozaic::modules::{Aggregator, Steplock, game}; + +mod planetwars; + // Load the config and start the game. fn main() { run(env::args().collect()); diff --git a/backend/src/planetwars/mod.rs b/backend/src/planetwars/mod.rs index e69de29..995343e 100644 --- a/backend/src/planetwars/mod.rs +++ b/backend/src/planetwars/mod.rs @@ -0,0 +1,124 @@ + +use mozaic::modules::game; + +use serde_json; + +use std::collections::HashMap; +use std::convert::TryInto; + +mod pw_config; +mod pw_serializer; +mod pw_rules; +mod pw_protocol; +use pw_protocol::{ self as proto, CommandError }; +use pw_rules::Dispatch; + + +pub struct PlanetWarsGame { + state: pw_rules::PlanetWars, + planet_map: HashMap, +} + +impl PlanetWarsGame { + + fn dispatch_state(&self, updates: &mut Vec, ) { + let state = pw_serializer::serialize(&self.state); + println!("{}", serde_json::to_string(&state).unwrap()); + + for player in self.state.players.iter() { + let state = pw_serializer::serialize_rotated(&self.state, player.id); + let state = if player.alive { + proto::ServerMessage::GameState(state) + } else { + proto::ServerMessage::FinalState(state) + }; + + updates.push( + game::Update::Player((player.id as u64).into(), serde_json::to_vec(&state).unwrap()) + ); + } + } + + fn execute_commands<'a>(&mut self, turns: Vec>, updates: &mut Vec) { + for (player_id, command) in turns.into_iter() { + let player_num: usize = (*player_id).try_into().unwrap(); + let action = proto::ServerMessage::PlayerAction(self.execute_action(player_num, command)); + let serialized_action = serde_json::to_vec(&action).unwrap(); + updates.push(game::Update::Player(player_id, serialized_action)); + } + } + + fn execute_action<'a>(&mut self, player_num: usize, turn: game::Turn<'a>) -> proto::PlayerAction { + let turn = match turn { + game::Turn::Timeout => return proto::PlayerAction::Timeout, + game::Turn::Action(bytes) => bytes, + }; + + let action: proto::Action = match serde_json::from_slice(&turn) { + Err(err) => return proto::PlayerAction::ParseError(err.to_string()), + Ok(action) => action, + }; + + let commands = action.commands.into_iter().map(|command| { + match self.check_valid_command(player_num, &command) { + Ok(dispatch) => { + self.state.dispatch(&dispatch); + proto::PlayerCommand { + command, + error: None, + } + }, + Err(error) => { + proto::PlayerCommand { + command, + error: Some(error), + } + } + } + }).collect(); + + return proto::PlayerAction::Commands(commands); + } + + fn check_valid_command(&self, player_num: usize, mv: &proto::Command) -> Result { + let origin_id = *self.planet_map + .get(&mv.origin) + .ok_or(CommandError::OriginDoesNotExist)?; + + let target_id = *self.planet_map + .get(&mv.destination) + .ok_or(CommandError::DestinationDoesNotExist)?; + + if self.state.planets[origin_id].owner() != Some(player_num) { + return Err(CommandError::OriginNotOwned); + } + + if self.state.planets[origin_id].ship_count() < mv.ship_count { + return Err(CommandError::NotEnoughShips); + } + + if mv.ship_count == 0 { + return Err(CommandError::ZeroShipMove); + } + + Ok(Dispatch { + origin: origin_id, + target: target_id, + ship_count: mv.ship_count, + }) + } +} + +impl game::GameController for PlanetWarsGame { + fn step<'a>(&mut self, turns: Vec>) -> Vec { + let mut updates = Vec::new(); + + self.state.repopulate(); + self.execute_commands(turns, &mut updates); + self.state.step(); + + self.dispatch_state(&mut updates); + + updates + } +} diff --git a/backend/src/pw/pw_config.rs b/backend/src/planetwars/pw_config.rs similarity index 88% rename from backend/src/pw/pw_config.rs rename to backend/src/planetwars/pw_config.rs index 694d0f4..a34aee3 100644 --- a/backend/src/pw/pw_config.rs +++ b/backend/src/planetwars/pw_config.rs @@ -7,10 +7,6 @@ use serde_json; use super::pw_protocol as proto; use super::pw_rules::*; -// TODO -use server::ClientId; - - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub map_file: String, @@ -18,7 +14,7 @@ pub struct Config { } impl Config { - pub fn create_game(&self, clients: Vec) -> PlanetWars { + pub fn create_game(&self, clients: Vec) -> PlanetWars { let planets = self.load_map(clients.len()); let players = clients.into_iter() .map(|client_id| Player { id: client_id, alive: true }) @@ -45,7 +41,7 @@ impl Config { let owner = planet.owner.and_then(|owner_num| { // in the current map format, player numbers start at 1. // TODO: we might want to change this. - let player_num = owner_num as usize - 1; + let player_num = owner_num - 1; // ignore players that are not in the game if player_num < num_players { Some(player_num) @@ -69,7 +65,7 @@ impl Config { }).collect(); } - fn read_map(&self) -> io::Result { + fn read_map(&self) -> io::Result { let mut file = File::open(&self.map_file)?; let mut buf = String::new(); file.read_to_string(&mut buf)?; @@ -77,3 +73,8 @@ impl Config { return Ok(map); } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Map { + pub planets: Vec, +} diff --git a/backend/src/pw/pw_protocol.rs b/backend/src/planetwars/pw_protocol.rs similarity index 74% rename from backend/src/pw/pw_protocol.rs rename to backend/src/planetwars/pw_protocol.rs index 9eb5c1c..23612d0 100644 --- a/backend/src/pw/pw_protocol.rs +++ b/backend/src/planetwars/pw_protocol.rs @@ -4,7 +4,7 @@ pub struct Expedition { pub ship_count: u64, pub origin: String, pub destination: String, - pub owner: u32, + pub owner: usize, pub turns_remaining: u64, } @@ -13,7 +13,7 @@ pub struct Planet { pub ship_count: u64, pub x: f64, pub y: f64, - pub owner: Option, + pub owner: Option, pub name: String, } @@ -30,11 +30,6 @@ pub struct Command { pub ship_count: u64, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Map { - pub planets: Vec, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct State { pub planets: Vec, @@ -82,24 +77,3 @@ pub enum ServerMessage { /// The game is over, and this is the concluding state. FinalState(State), } - -// lobby messages -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -#[serde(tag = "type", content = "content")] -pub enum ControlMessage { - PlayerConnected { - player_id: u64, - }, - PlayerDisconnected { - player_id: u64, - }, - GameState(State), -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -#[serde(tag = "type", content = "content")] -pub enum LobbyCommand { - StartMatch, -} diff --git a/backend/src/pw/pw_rules.rs b/backend/src/planetwars/pw_rules.rs similarity index 98% rename from backend/src/pw/pw_rules.rs rename to backend/src/planetwars/pw_rules.rs index ed66b26..cfa059a 100644 --- a/backend/src/pw/pw_rules.rs +++ b/backend/src/planetwars/pw_rules.rs @@ -1,4 +1,3 @@ -use server::ClientId; /// The planet wars game rules. pub struct PlanetWars { @@ -14,7 +13,7 @@ pub struct PlanetWars { #[derive(Debug)] pub struct Player { - pub id: ClientId, + pub id: usize, pub alive: bool, } @@ -136,7 +135,7 @@ impl PlanetWars { return remaining < 2 || self.turn_num >= self.max_turns; } - pub fn living_players(&self) -> Vec { + pub fn living_players(&self) -> Vec { self.players.iter().filter_map(|p| { if p.alive { Some(p.id) @@ -197,4 +196,4 @@ impl Planet { let dy = self.y - other.y; return (dx.powi(2) + dy.powi(2)).sqrt().ceil() as u64; } -} \ No newline at end of file +} diff --git a/backend/src/pw/pw_serializer.rs b/backend/src/planetwars/pw_serializer.rs similarity index 51% rename from backend/src/pw/pw_serializer.rs rename to backend/src/planetwars/pw_serializer.rs index c258f7e..c0225df 100644 --- a/backend/src/pw/pw_serializer.rs +++ b/backend/src/planetwars/pw_serializer.rs @@ -1,20 +1,28 @@ + use super::pw_rules::{PlanetWars, Planet, Expedition}; use super::pw_protocol as proto; /// Serialize given gamestate -pub fn serialize_state(state: &PlanetWars) -> proto::State { - let serializer = Serializer::new(state); +pub fn serialize(state: &PlanetWars) -> proto::State { + serialize_rotated(state, 0) +} + +/// Serialize given gamestate with player numbers rotated by given offset. +pub fn serialize_rotated(state: &PlanetWars, offset: usize) -> proto::State { + let serializer = Serializer::new(state, offset); serializer.serialize_state() } struct Serializer<'a> { state: &'a PlanetWars, + player_num_offset: usize, } impl<'a> Serializer<'a> { - fn new(state: &'a PlanetWars) -> Self { + fn new(state: &'a PlanetWars, offset: usize) -> Self { Serializer { state: state, + player_num_offset: offset, } } @@ -33,9 +41,14 @@ impl<'a> Serializer<'a> { } } - // gets the client id for a player number - fn player_client_id(&self, player_num: usize) -> u32 { - self.state.players[player_num].id.as_u32() + /// Gets the player number for given player id. + /// Player numbers are 1-based (as opposed to player ids), They will also be + /// rotated based on the number offset for this serializer. + fn player_num(&self, player_id: usize) -> usize { + let num_players = self.state.players.len(); + let rotated_id = (player_id + self.player_num_offset) % num_players; + // protocol player ids start at 1 + return rotated_id + 1; } fn serialize_planet(&self, planet: &Planet) -> proto::Planet { @@ -43,7 +56,7 @@ impl<'a> Serializer<'a> { name: planet.name.clone(), x: planet.x, y: planet.y, - owner: planet.owner().map(|num| self.player_client_id(num)), + owner: planet.owner().map(|id| self.player_num(id)), ship_count: planet.ship_count(), } } @@ -51,10 +64,10 @@ impl<'a> Serializer<'a> { fn serialize_expedition(&self, exp: &Expedition) -> proto::Expedition { proto::Expedition { id: exp.id, - owner: self.player_client_id(exp.fleet.owner.unwrap()), + owner: self.player_num(exp.fleet.owner.unwrap()), ship_count: exp.fleet.ship_count, - origin: self.state.planets[exp.origin].name.clone(), - destination: self.state.planets[exp.target].name.clone(), + origin: self.state.planets[exp.origin as usize].name.clone(), + destination: self.state.planets[exp.target as usize].name.clone(), turns_remaining: exp.turns_remaining, } } diff --git a/backend/src/pw/mod.rs b/backend/src/pw/mod.rs deleted file mode 100644 index 9e1ade5..0000000 --- a/backend/src/pw/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ - -pub mod pw_protocol; -mod pw_controller; -mod pw_rules; -mod pw_config; -mod pw_serializer; - -pub use self::pw_controller::PwMatch; -pub use self::pw_rules::PlanetWars; -pub use self::pw_config::Config; -pub use self::pw_protocol::Map; diff --git a/backend/src/pw/pw_controller.rs b/backend/src/pw/pw_controller.rs deleted file mode 100644 index 1a7ac5f..0000000 --- a/backend/src/pw/pw_controller.rs +++ /dev/null @@ -1,452 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::time::{Duration, Instant}; -use std::mem; -use std::io; - -use events; -use network::server::RegisteredHandle; -use reactors::reactor::ReactorHandle; -use reactors::{WireEvent, RequestHandler}; -use server::{ClientId, ConnectionManager}; -use sodiumoxide::crypto::sign::PublicKey; - -use super::Config; -use super::pw_rules::{PlanetWars, Dispatch}; -use super::pw_serializer::serialize_state; -use super::pw_protocol::{ - self as proto, - PlayerAction, - PlayerCommand, - CommandError, -}; - -use serde_json; - -pub struct Player { - id: ClientId, - num: usize, - handle: RegisteredHandle, -} - -pub struct ClientHandler { - client_id: u32, - reactor_handle: ReactorHandle, -} - -impl ClientHandler { - pub fn new(client_id: u32, reactor_handle: ReactorHandle) -> Self { - ClientHandler { - client_id, - reactor_handle, - } - } - - pub fn on_connect(&mut self, _event: &events::Connected) - -> io::Result - { - self.reactor_handle.dispatch(events::ClientConnected { - client_id: self.client_id, - }); - Ok(WireEvent::null()) - } - - pub fn on_disconnect(&mut self, _event: &events::Disconnected) - -> io::Result - { - self.reactor_handle.dispatch(events::ClientDisconnected { - client_id: self.client_id, - }); - Ok(WireEvent::null()) - } - - pub fn on_message(&mut self, event: &events::ClientSend) - -> io::Result - { - self.reactor_handle.dispatch(events::ClientMessage { - client_id: self.client_id, - data: event.data.clone(), - }); - Ok(WireEvent::null()) - } -} - -pub struct PwMatch { - state: PwMatchState, -} - -enum PwMatchState { - Lobby(Lobby), - Playing(PwController), - Finished, -} - -impl PwMatch { - pub fn new(match_uuid: Vec, - reactor_handle: ReactorHandle, - connection_manager: ConnectionManager) - -> Self - { - let lobby = Lobby::new( - match_uuid, - connection_manager, - reactor_handle - ); - - return PwMatch { - state: PwMatchState::Lobby(lobby), - } - } - - fn take_state(&mut self) -> PwMatchState { - mem::replace(&mut self.state, PwMatchState::Finished) - } - - pub fn register_client(&mut self, event: &events::RegisterClient) { - if let &mut PwMatchState::Lobby(ref mut lobby) = &mut self.state { - let client_id = ClientId::new(event.client_id); - let key = PublicKey::from_slice(&event.public_key).unwrap(); - lobby.add_player(client_id, key); - } - } - - pub fn remove_client(&mut self, event: &events::RemoveClient) { - if let PwMatchState::Lobby(ref mut lobby) = self.state { - let client_id = ClientId::new(event.client_id); - lobby.remove_player(client_id); - } - } - - pub fn start_game(&mut self, event: &events::StartGame) { - let state = self.take_state(); - - if let PwMatchState::Lobby(lobby) = state { - let config = Config { - map_file: event.map_path.clone(), - max_turns: event.max_turns as u64, - }; - self.state = PwMatchState::Playing(PwController::new( - config, - lobby.reactor_handle, - lobby.connection_manager, - lobby.players, - )); - } else { - self.state = state; - } - } - - pub fn game_step(&mut self, event: &events::GameStep) { - if let PwMatchState::Playing(ref mut controller) = self.state { - controller.on_step(event); - } - } - - pub fn client_message(&mut self, event: &events::ClientMessage) { - if let PwMatchState::Playing(ref mut controller) = self.state { - controller.on_client_message(event); - } - } - - pub fn game_finished(&mut self, event: &events::GameFinished) { - let state = self.take_state(); - if let PwMatchState::Playing(mut controller) = state { - controller.on_finished(event); - } - self.state = PwMatchState::Finished; - } - - pub fn timeout(&mut self, event: &events::TurnTimeout) { - if let PwMatchState::Playing(ref mut controller) = self.state { - controller.on_timeout(event); - } - } -} - -pub struct Lobby { - match_uuid: Vec, - connection_manager: ConnectionManager, - reactor_handle: ReactorHandle, - - players: HashMap, -} - -impl Lobby { - fn new(match_uuid: Vec, - connection_manager: ConnectionManager, - reactor_handle: ReactorHandle) - -> Self - { - return Lobby { - match_uuid, - connection_manager, - reactor_handle, - - players: HashMap::new(), - // start counter at 1, because 0 is the control client - } - } - - fn add_player(&mut self, client_id: ClientId, public_key: PublicKey) { - let mut core = RequestHandler::new( - ClientHandler::new( - client_id.as_u32(), - self.reactor_handle.clone(), - ), - ); - - core.add_handler(ClientHandler::on_connect); - core.add_handler(ClientHandler::on_disconnect); - core.add_handler(ClientHandler::on_message); - - let handle = self.connection_manager.create_client( - self.match_uuid.clone(), - client_id.as_u32(), - public_key, - core - ); - - self.players.insert(client_id, handle); - } - - fn remove_player(&mut self, client_id: ClientId) { - self.players.remove(&client_id); - } -} - -pub struct PwController { - state: PlanetWars, - planet_map: HashMap, - reactor_handle: ReactorHandle, - connection_manager: ConnectionManager, - - client_player: HashMap, - players: HashMap, - - waiting_for: HashSet, - commands: HashMap, -} - -impl PwController { - pub fn new(config: Config, - reactor_handle: ReactorHandle, - connection_manager: ConnectionManager, - clients: HashMap) - -> Self - { - // TODO: we probably want a way to fixate player order - let client_ids = clients.keys().cloned().collect(); - let state = config.create_game(client_ids); - - let planet_map = state.planets.iter().map(|planet| { - (planet.name.clone(), planet.id) - }).collect(); - - let mut client_player = HashMap::new(); - let mut players = HashMap::new(); - - let clients_iter = clients.into_iter().enumerate(); - for (player_num, (client_id, client_handle)) in clients_iter { - client_player.insert(client_id, player_num); - players.insert(client_id, Player { - id: client_id, - num: player_num, - handle: client_handle, - }); - } - - let mut controller = PwController { - state, - planet_map, - players, - client_player, - reactor_handle, - connection_manager, - - waiting_for: HashSet::new(), - commands: HashMap::new(), - }; - // this initial dispatch starts the game - controller.dispatch_state(); - return controller; - } - - - /// Advance the game by one turn. - fn step(&mut self) { - self.state.repopulate(); - self.execute_commands(); - self.state.step(); - - self.dispatch_state(); - } - - fn dispatch_state(&mut self) { - let turn_num = self.state.turn_num; - // TODO: fix this - let state = serde_json::to_string(&serialize_state(&self.state)).unwrap(); - - if self.state.is_finished() { - let event = events::GameFinished { turn_num, state }; - self.reactor_handle.dispatch(event); - } else { - let event = events::GameStep { turn_num, state }; - self.reactor_handle.dispatch(event); - } - } - - fn on_step(&mut self, step: &events::GameStep) { - let state = &self.state; - let waiting_for = &mut self.waiting_for; - - self.players.retain(|_, player| { - if state.players[player.num].alive { - waiting_for.insert(player.id); - player.handle.dispatch(events::GameStep { - turn_num: step.turn_num, - state: step.state.clone(), - }); - // keep this player in the game - return true; - } else { - player.handle.dispatch(events::GameFinished { - turn_num: step.turn_num, - state: step.state.clone(), - }); - // this player is dead, kick him! - // TODO: shutdown the reactor - // TODO: todo - return false; - } - }); - - let deadline = Instant::now() + Duration::from_secs(1); - self.reactor_handle.dispatch_at(deadline, events::TurnTimeout { - turn_num: state.turn_num, - }); - } - - fn on_client_message(&mut self, event: &events::ClientMessage) { - let client_id = ClientId::new(event.client_id); - self.waiting_for.remove(&client_id); - self.commands.insert(client_id, event.data.clone()); - - if self.waiting_for.is_empty() { - self.step(); - } - } - - fn on_finished(&mut self, event: &events::GameFinished) { - self.players.retain(|_player_id, player| { - player.handle.dispatch(events::GameFinished { - turn_num: event.turn_num, - state: event.state.clone(), - }); - // game is over, kick everyone. - false - }); - println!("everybody has been kicked"); - self.reactor_handle.quit(); - } - - fn on_timeout(&mut self, event: &events::TurnTimeout) { - if self.state.turn_num == event.turn_num { - self.step(); - } - } - - fn player_commands(&mut self) -> HashMap> { - let commands = &mut self.commands; - return self.players.values().map(|player| { - let command = commands.remove(&player.id); - return (player.id, command); - }).collect(); - } - - fn execute_commands(&mut self) { - let mut commands = self.player_commands(); - - for (player_id, command) in commands.drain() { - let player_num = self.players[&player_id].num; - let action = self.execute_action(player_num, command); - let serialized_action = serde_json::to_string(&action).unwrap(); - self.reactor_handle.dispatch(events::PlayerAction { - client_id: player_id.as_u32(), - action: serialized_action.clone(), - }); - self.players - .get_mut(&player_id) - .unwrap() - .handle - .dispatch(events::PlayerAction { - client_id: player_id.as_u32(), - action: serialized_action, - }); - } - } - - fn execute_action(&mut self, player_num: usize, response: Option) - -> PlayerAction - { - // TODO: it would be cool if this could be done with error_chain. - - let message = match response { - None => return PlayerAction::Timeout, - Some(message) => message, - }; - - let action: proto::Action = match serde_json::from_str(&message) { - Err(err) => return PlayerAction::ParseError(err.to_string()), - Ok(action) => action, - }; - - let commands = action.commands.into_iter().map(|command| { - match self.parse_command(player_num, &command) { - Ok(dispatch) => { - self.state.dispatch(&dispatch); - PlayerCommand { - command, - error: None, - } - }, - Err(error) => { - PlayerCommand { - command, - error: Some(error), - } - } - } - }).collect(); - - return PlayerAction::Commands(commands); - } - - fn parse_command(&self, player_num: usize, mv: &proto::Command) - -> Result - { - let origin_id = *self.planet_map - .get(&mv.origin) - .ok_or(CommandError::OriginDoesNotExist)?; - - let target_id = *self.planet_map - .get(&mv.destination) - .ok_or(CommandError::DestinationDoesNotExist)?; - - if self.state.planets[origin_id].owner() != Some(player_num) { - return Err(CommandError::OriginNotOwned); - } - - if self.state.planets[origin_id].ship_count() < mv.ship_count { - return Err(CommandError::NotEnoughShips); - } - - if mv.ship_count == 0 { - return Err(CommandError::ZeroShipMove); - } - - Ok(Dispatch { - origin: origin_id, - target: target_id, - ship_count: mv.ship_count, - }) - } -} \ No newline at end of file From fc6bcca0f8dd36bf51f37c45be5eae95c3567633 Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Sat, 14 Sep 2019 20:16:20 +0200 Subject: [PATCH 3/8] don't waste peoples time --- backend/src/planetwars/mod.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/src/planetwars/mod.rs b/backend/src/planetwars/mod.rs index 995343e..90d0e24 100644 --- a/backend/src/planetwars/mod.rs +++ b/backend/src/planetwars/mod.rs @@ -21,11 +21,11 @@ pub struct PlanetWarsGame { impl PlanetWarsGame { - fn dispatch_state(&self, updates: &mut Vec, ) { + fn dispatch_state(&self, were_alive: Vec, updates: &mut Vec, ) { let state = pw_serializer::serialize(&self.state); println!("{}", serde_json::to_string(&state).unwrap()); - for player in self.state.players.iter() { + for player in self.state.players.iter().filter(|p| were_alive.contains(&p.id)) { let state = pw_serializer::serialize_rotated(&self.state, player.id); let state = if player.alive { proto::ServerMessage::GameState(state) @@ -36,6 +36,10 @@ impl PlanetWarsGame { updates.push( game::Update::Player((player.id as u64).into(), serde_json::to_vec(&state).unwrap()) ); + + if !player.alive { + updates.push(game::Update::Kick((player.id as u64).into())); + } } } @@ -113,11 +117,13 @@ impl game::GameController for PlanetWarsGame { fn step<'a>(&mut self, turns: Vec>) -> Vec { let mut updates = Vec::new(); + let alive = self.state.living_players(); + self.state.repopulate(); self.execute_commands(turns, &mut updates); self.state.step(); - self.dispatch_state(&mut updates); + self.dispatch_state(alive, &mut updates); updates } From 2ad7354de5b48d9b943881f755bfabb02b11c580 Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Sat, 14 Sep 2019 21:16:29 +0200 Subject: [PATCH 4/8] add creation handling --- backend/map.json | 1 + backend/src/main.rs | 34 +++++++++-------------------- backend/src/planetwars/mod.rs | 10 ++++++++- backend/src/planetwars/pw_config.rs | 6 ++--- 4 files changed, 23 insertions(+), 28 deletions(-) create mode 100644 backend/map.json diff --git a/backend/map.json b/backend/map.json new file mode 100644 index 0000000..b7be71d --- /dev/null +++ b/backend/map.json @@ -0,0 +1 @@ +{"planets":[{"name":"PIET9+0","x":27,"y":0,"ship_count":6},{"name":"PIET10+0","x":30,"y":0,"ship_count":6},{"name":"PIET11+0","x":33,"y":0,"ship_count":6},{"name":"PIET12+0","x":36,"y":0,"ship_count":6},{"name":"PIET13+0","x":39,"y":0,"ship_count":6},{"name":"PIET7+1","x":21,"y":3,"ship_count":6},{"name":"PIET8+1","x":24,"y":3,"ship_count":6},{"name":"PIET14+1","x":42,"y":3,"ship_count":6},{"name":"PIET6+2","x":18,"y":6,"ship_count":6},{"name":"PIET15+2","x":45,"y":6,"ship_count":6},{"name":"PIET6+3","x":18,"y":9,"ship_count":6},{"name":"PIET7+3","x":21,"y":9,"ship_count":6},{"name":"PIET8+3","x":24,"y":9,"ship_count":6},{"name":"PIET11+3","x":33,"y":9,"ship_count":6},{"name":"PIET12+3","x":36,"y":9,"ship_count":6},{"name":"PIET15+3","x":45,"y":9,"ship_count":6},{"name":"PIET6+4","x":18,"y":12,"ship_count":6},{"name":"PIET7+4","x":21,"y":12,"ship_count":6,"owner":1},{"name":"PIET11+4","x":33,"y":12,"ship_count":6},{"name":"PIET15+4","x":45,"y":12,"ship_count":6},{"name":"PIET5+5","x":15,"y":15,"ship_count":6},{"name":"PIET9+5","x":27,"y":15,"ship_count":6},{"name":"PIET14+5","x":42,"y":15,"ship_count":6,"owner":2},{"name":"PIET5+6","x":15,"y":18,"ship_count":6},{"name":"PIET6+6","x":18,"y":18,"ship_count":6},{"name":"PIET7+6","x":21,"y":18,"ship_count":6},{"name":"PIET8+6","x":24,"y":18,"ship_count":6},{"name":"PIET9+6","x":27,"y":18,"ship_count":6},{"name":"PIET10+6","x":30,"y":18,"ship_count":6},{"name":"PIET11+6","x":33,"y":18,"ship_count":6},{"name":"PIET14+6","x":42,"y":18,"ship_count":6},{"name":"PIET4+7","x":12,"y":21,"ship_count":6},{"name":"PIET5+7","x":15,"y":21,"ship_count":6},{"name":"PIET14+7","x":42,"y":21,"ship_count":6},{"name":"PIET4+8","x":12,"y":24,"ship_count":6},{"name":"PIET13+8","x":39,"y":24,"ship_count":6},{"name":"PIET14+8","x":42,"y":24,"ship_count":6},{"name":"PIET4+9","x":12,"y":27,"ship_count":6},{"name":"PIET13+9","x":39,"y":27,"ship_count":6},{"name":"PIET4+10","x":12,"y":30,"ship_count":6},{"name":"PIET10+10","x":30,"y":30,"ship_count":6},{"name":"PIET11+10","x":33,"y":30,"ship_count":6},{"name":"PIET13+10","x":39,"y":30,"ship_count":6},{"name":"PIET21+10","x":63,"y":30,"ship_count":6},{"name":"PIET22+10","x":66,"y":30,"ship_count":6},{"name":"PIET4+11","x":12,"y":33,"ship_count":6},{"name":"PIET10+11","x":30,"y":33,"ship_count":6},{"name":"PIET11+11","x":33,"y":33,"ship_count":6},{"name":"PIET13+11","x":39,"y":33,"ship_count":6},{"name":"PIET19+11","x":57,"y":33,"ship_count":6},{"name":"PIET20+11","x":60,"y":33,"ship_count":6},{"name":"PIET22+11","x":66,"y":33,"ship_count":6},{"name":"PIET4+12","x":12,"y":36,"ship_count":6},{"name":"PIET10+12","x":30,"y":36,"ship_count":6},{"name":"PIET13+12","x":39,"y":36,"ship_count":6},{"name":"PIET14+12","x":42,"y":36,"ship_count":6},{"name":"PIET18+12","x":54,"y":36,"ship_count":6},{"name":"PIET22+12","x":66,"y":36,"ship_count":6},{"name":"PIET4+13","x":12,"y":39,"ship_count":6},{"name":"PIET9+13","x":27,"y":39,"ship_count":6},{"name":"PIET10+13","x":30,"y":39,"ship_count":6},{"name":"PIET13+13","x":39,"y":39,"ship_count":6},{"name":"PIET15+13","x":45,"y":39,"ship_count":6},{"name":"PIET17+13","x":51,"y":39,"ship_count":6},{"name":"PIET18+13","x":54,"y":39,"ship_count":6},{"name":"PIET21+13","x":63,"y":39,"ship_count":6},{"name":"PIET4+14","x":12,"y":42,"ship_count":6},{"name":"PIET9+14","x":27,"y":42,"ship_count":6},{"name":"PIET10+14","x":30,"y":42,"ship_count":6},{"name":"PIET13+14","x":39,"y":42,"ship_count":6},{"name":"PIET16+14","x":48,"y":42,"ship_count":6},{"name":"PIET20+14","x":60,"y":42,"ship_count":6},{"name":"PIET4+15","x":12,"y":45,"ship_count":6},{"name":"PIET8+15","x":24,"y":45,"ship_count":6},{"name":"PIET10+15","x":30,"y":45,"ship_count":6},{"name":"PIET15+15","x":45,"y":45,"ship_count":6},{"name":"PIET16+15","x":48,"y":45,"ship_count":6},{"name":"PIET19+15","x":57,"y":45,"ship_count":6},{"name":"PIET4+16","x":12,"y":48,"ship_count":6},{"name":"PIET14+16","x":42,"y":48,"ship_count":6},{"name":"PIET17+16","x":51,"y":48,"ship_count":6},{"name":"PIET20+16","x":60,"y":48,"ship_count":6},{"name":"PIET4+17","x":12,"y":51,"ship_count":6},{"name":"PIET5+17","x":15,"y":51,"ship_count":6},{"name":"PIET18+17","x":54,"y":51,"ship_count":6},{"name":"PIET19+17","x":57,"y":51,"ship_count":6},{"name":"PIET5+18","x":15,"y":54,"ship_count":6},{"name":"PIET6+18","x":18,"y":54,"ship_count":6},{"name":"PIET17+18","x":51,"y":54,"ship_count":6},{"name":"PIET6+19","x":18,"y":57,"ship_count":6},{"name":"PIET7+19","x":21,"y":57,"ship_count":6},{"name":"PIET17+19","x":51,"y":57,"ship_count":6},{"name":"PIET6+20","x":18,"y":60,"ship_count":6},{"name":"PIET8+20","x":24,"y":60,"ship_count":6},{"name":"PIET16+20","x":48,"y":60,"ship_count":6},{"name":"PIET17+20","x":51,"y":60,"ship_count":6},{"name":"PIET3+21","x":9,"y":63,"ship_count":6},{"name":"PIET5+21","x":15,"y":63,"ship_count":6},{"name":"PIET6+21","x":18,"y":63,"ship_count":6},{"name":"PIET9+21","x":27,"y":63,"ship_count":6},{"name":"PIET10+21","x":30,"y":63,"ship_count":6},{"name":"PIET11+21","x":33,"y":63,"ship_count":6},{"name":"PIET12+21","x":36,"y":63,"ship_count":6},{"name":"PIET13+21","x":39,"y":63,"ship_count":6},{"name":"PIET14+21","x":42,"y":63,"ship_count":6},{"name":"PIET15+21","x":45,"y":63,"ship_count":6},{"name":"PIET4+22","x":12,"y":66,"ship_count":6},{"name":"PIET5+22","x":15,"y":66,"ship_count":6},{"name":"PIET8+22","x":24,"y":66,"ship_count":6},{"name":"PIET11+22","x":33,"y":66,"ship_count":6},{"name":"PIET9+23","x":27,"y":69,"ship_count":6},{"name":"PIET11+23","x":33,"y":69,"ship_count":6},{"name":"PIET10+24","x":30,"y":72,"ship_count":6}]} \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs index c2f7495..cb9b5ab 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -23,27 +23,6 @@ fn main() { run(env::args().collect()); } -use std::str; -struct Server; -impl game::GameController for Server { - fn step<'a>(&mut self, turns: Vec>) -> Vec { - let mut out = Vec::new(); - - for (id, turn) in turns.iter() { - let postfix = match turn { - game::Turn::Action(bytes) => str::from_utf8(bytes).unwrap(), - game::Turn::Timeout => "Timed out", - }; - - let msg = format!("{}: {}", **id, postfix); - - out.push(game::Update::Global(msg.as_bytes().to_vec())); - } - - return out; - } -} - use mozaic::server::runtime::{Broker}; use rand::Rng; use errors::Consumable; @@ -62,14 +41,21 @@ pub fn run(args : Vec) { let number_of_clients = args.get(1).map(|x| x.parse().unwrap_or(1)).unwrap_or(1); - let ids: HashMap<_, util::PlayerId> = (0..number_of_clients).map(|x| (x.into(), (10 - x).into())).collect(); + let ids: HashMap = (0..number_of_clients).map(|x| (rand::thread_rng().gen::().into(), x.into())).collect(); - println!("Ids: {:?}", ids); + let config = planetwars::Config { map_file: String::from("map.json"), max_turns: 500 }; + let game = planetwars::PlanetWarsGame::new(config.create_game(number_of_clients as usize)); + + println!("Tokens:"); + let keys: Vec = ids.keys().map(|&x| x.into()).collect(); + for key in keys { + println!("key {}", key); + } tokio::run(futures::lazy(move || { let mut broker = Broker::new().unwrap(); - broker.spawn(welcomer_id.clone(), game::GameReactor::params(steplock_id.clone(), Box::new(Server)), "Main").display(); + broker.spawn(welcomer_id.clone(), game::GameReactor::params(steplock_id.clone(), Box::new(game)), "Main").display(); broker.spawn(steplock_id.clone(), Steplock::new(broker.clone(), ids.values().cloned().collect(), welcomer_id.clone(), aggregator_id.clone()).with_timeout(5000).params(), "Steplock").display(); broker.spawn(aggregator_id.clone(), Aggregator::params(manager_id.clone(), steplock_id.clone()), "Aggregator").display(); broker.spawn( diff --git a/backend/src/planetwars/mod.rs b/backend/src/planetwars/mod.rs index 90d0e24..fe195e3 100644 --- a/backend/src/planetwars/mod.rs +++ b/backend/src/planetwars/mod.rs @@ -12,7 +12,7 @@ mod pw_rules; mod pw_protocol; use pw_protocol::{ self as proto, CommandError }; use pw_rules::Dispatch; - +pub use pw_config::Config; pub struct PlanetWarsGame { state: pw_rules::PlanetWars, @@ -21,6 +21,14 @@ pub struct PlanetWarsGame { impl PlanetWarsGame { + pub fn new(state: pw_rules::PlanetWars) -> Self { + let planet_map = state.planets.iter().map(|p| (p.name.clone(), p.id)).collect(); + + Self { + state, planet_map + } + } + fn dispatch_state(&self, were_alive: Vec, updates: &mut Vec, ) { let state = pw_serializer::serialize(&self.state); println!("{}", serde_json::to_string(&state).unwrap()); diff --git a/backend/src/planetwars/pw_config.rs b/backend/src/planetwars/pw_config.rs index a34aee3..efe99e5 100644 --- a/backend/src/planetwars/pw_config.rs +++ b/backend/src/planetwars/pw_config.rs @@ -14,9 +14,9 @@ pub struct Config { } impl Config { - pub fn create_game(&self, clients: Vec) -> PlanetWars { - let planets = self.load_map(clients.len()); - let players = clients.into_iter() + pub fn create_game(&self, clients: usize) -> PlanetWars { + let planets = self.load_map(clients); + let players = (0..clients) .map(|client_id| Player { id: client_id, alive: true }) .collect(); From c846042597c17b602278cca5577f2f3b2b6ca309 Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Mon, 16 Sep 2019 21:18:01 +0200 Subject: [PATCH 5/8] wtf --- backend/src/main.rs | 2 +- backend/src/planetwars/mod.rs | 17 +- client/Cargo.toml | 8 + client/src/main.rs | 289 ++++++++++++++++++++- frontend/www/index.js | 1 + frontend/www/package-lock.json | 79 +++++- frontend/www/package.json | 9 +- frontend/www/tsconfig.json | 15 ++ frontend/www/webgl/buffer.ts | 55 ++++ frontend/www/webgl/instance.ts | 72 ++++++ frontend/www/webgl/renderer.ts | 72 ++++++ frontend/www/webgl/shader.ts | 305 +++++++++++++++++++++++ frontend/www/webgl/texture.ts | 68 +++++ frontend/www/webgl/util.ts | 129 ++++++++++ frontend/www/webgl/vertexArray.ts | 0 frontend/www/webgl/vertexBufferLayout.ts | 112 +++++++++ frontend/www/webpack.config.js | 22 +- 17 files changed, 1239 insertions(+), 16 deletions(-) create mode 100644 frontend/www/tsconfig.json create mode 100644 frontend/www/webgl/buffer.ts create mode 100644 frontend/www/webgl/instance.ts create mode 100644 frontend/www/webgl/renderer.ts create mode 100644 frontend/www/webgl/shader.ts create mode 100644 frontend/www/webgl/texture.ts create mode 100644 frontend/www/webgl/util.ts create mode 100644 frontend/www/webgl/vertexArray.ts create mode 100644 frontend/www/webgl/vertexBufferLayout.ts diff --git a/backend/src/main.rs b/backend/src/main.rs index cb9b5ab..8dcb2ae 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -43,7 +43,7 @@ pub fn run(args : Vec) { let ids: HashMap = (0..number_of_clients).map(|x| (rand::thread_rng().gen::().into(), x.into())).collect(); - let config = planetwars::Config { map_file: String::from("map.json"), max_turns: 500 }; + let config = planetwars::Config { map_file: String::from("hex.json"), max_turns: 500 }; let game = planetwars::PlanetWarsGame::new(config.create_game(number_of_clients as usize)); println!("Tokens:"); diff --git a/backend/src/planetwars/mod.rs b/backend/src/planetwars/mod.rs index fe195e3..3f02d4b 100644 --- a/backend/src/planetwars/mod.rs +++ b/backend/src/planetwars/mod.rs @@ -5,6 +5,8 @@ use serde_json; use std::collections::HashMap; use std::convert::TryInto; +use std::fs::File; +use std::io::Write; mod pw_config; mod pw_serializer; @@ -17,25 +19,30 @@ pub use pw_config::Config; pub struct PlanetWarsGame { state: pw_rules::PlanetWars, planet_map: HashMap, + log_file: File } impl PlanetWarsGame { pub fn new(state: pw_rules::PlanetWars) -> Self { let planet_map = state.planets.iter().map(|p| (p.name.clone(), p.id)).collect(); + let file = File::create("game.json").unwrap(); Self { - state, planet_map + state, planet_map, + log_file: file, } } - fn dispatch_state(&self, were_alive: Vec, updates: &mut Vec, ) { + fn dispatch_state(&mut self, were_alive: Vec, updates: &mut Vec, ) { let state = pw_serializer::serialize(&self.state); - println!("{}", serde_json::to_string(&state).unwrap()); + write!(self.log_file, "{}\n", serde_json::to_string(&state).unwrap()).unwrap(); + + // println!("{}", serde_json::to_string(&state).unwrap()); for player in self.state.players.iter().filter(|p| were_alive.contains(&p.id)) { let state = pw_serializer::serialize_rotated(&self.state, player.id); - let state = if player.alive { + let state = if player.alive && !self.state.is_finished() { proto::ServerMessage::GameState(state) } else { proto::ServerMessage::FinalState(state) @@ -45,7 +52,7 @@ impl PlanetWarsGame { game::Update::Player((player.id as u64).into(), serde_json::to_vec(&state).unwrap()) ); - if !player.alive { + if !player.alive || self.state.is_finished() { updates.push(game::Update::Kick((player.id as u64).into())); } } diff --git a/client/Cargo.toml b/client/Cargo.toml index 594bb4a..508e5e1 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -7,3 +7,11 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +mozaic = { git = "https://github.com/ajuvercr/MOZAIC" } +tokio = "0.1.22" +capnp = "0.10.1" +futures = "0.1.28" +serde = "1.0.100" +serde_derive = "1.0.100" +serde_json = "1.0" +rand = { version = "0.6.5", default-features = true } diff --git a/client/src/main.rs b/client/src/main.rs index e7a11a9..fbc15d5 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,3 +1,290 @@ +extern crate mozaic; +extern crate tokio; +extern crate futures; +extern crate capnp; +extern crate rand; + +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; + +use rand::Rng; + +use mozaic::core_capnp::{initialize, terminate_stream, identify, actor_joined}; +use mozaic::messaging::reactor::*; +use mozaic::messaging::types::*; +use mozaic::errors::*; +use mozaic::client_capnp::{client_message, host_message, client_kicked}; +use mozaic::mozaic_cmd_capnp::{bot_input, bot_return}; +use mozaic::server::runtime::{Broker, BrokerHandle}; +use mozaic::server; +use mozaic::modules::{BotReactor}; + +use std::env; +use std::str; + +mod types; + fn main() { - println!("Hello, world!"); + let args: Vec = env::args().collect(); + let id = args.get(1).unwrap().parse().unwrap(); + let client_args = args.get(2..).expect("How do you expect me to spawn your bot?").to_vec(); + + let addr = "127.0.0.1:9142".parse().unwrap(); + let self_id: ReactorId = rand::thread_rng().gen(); + + tokio::run(futures::lazy(move || { + let mut broker = Broker::new().unwrap(); + let reactor = ClientReactor { + server: None, + id, + broker: broker.clone(), + args: client_args, + }; + broker.spawn(self_id.clone(), reactor.params(), "main").display(); + + tokio::spawn(server::connect_to_server(broker, self_id, &addr)); + Ok(()) + })); +} + +// Main client logic +/// ? greeter_id is the server, the tcp stream that you connected to +/// ? runtime_id is your own runtime, to handle visualisation etc +/// ? user are you ... +struct ClientReactor { + server: Option, + broker: BrokerHandle, + id: u64, + args: Vec, +} + +impl ClientReactor { + fn params(self) -> CoreParams { + let mut params = CoreParams::new(self); + params.handler(initialize::Owned, CtxHandler::new(Self::initialize)); + params.handler(actor_joined::Owned, CtxHandler::new(Self::open_host)); + return params; + } + + // reactor setup + fn initialize( + &mut self, + handle: &mut ReactorHandle, + _: initialize::Reader, + ) -> Result<()> + { + // open link with runtime, for communicating with chat GUI + let runtime_link = RuntimeLink::params(handle.id().clone()); + handle.open_link(runtime_link)?; + + let bot = BotReactor::new(self.broker.clone(), handle.id().clone(), self.args.clone()); + let bot_id = handle.spawn(bot.params(), "Bot Driver")?; + + handle.open_link(BotLink::params(bot_id))?; + + return Ok(()); + } + + fn open_host( + &mut self, + handle: &mut ReactorHandle, + r: actor_joined::Reader, + ) -> Result<()> + { + let id = r.get_id()?; + + if let Some(server) = &self.server { + handle.open_link(HostLink::params(ReactorId::from(id)))?; + self.broker.register_as(id.into(), server.clone()); + + // Fake bot msg + let mut chat_message = MsgBuffer::::new(); + chat_message.build(|b| { + b.set_message(b""); + }); + handle.send_internal(chat_message)?; + + } else { + + handle.open_link(ServerLink::params(id.into()))?; + self.server = Some(id.into()); + + let mut identify = MsgBuffer::::new(); + identify.build(|b| { + b.set_key(self.id); + }); + handle.send_internal(identify).display(); + + } + + Ok(()) + } +} + +// Handler for the connection with the chat server +struct ServerLink; +impl ServerLink { + fn params(foreign_id: ReactorId) -> LinkParams { + let mut params = LinkParams::new(foreign_id, Self); + + params.external_handler( terminate_stream::Owned, CtxHandler::new(Self::close_handler) ); + params.external_handler( actor_joined::Owned, CtxHandler::new(actor_joined::e_to_i) ); + + params.internal_handler( identify::Owned, CtxHandler::new(Self::identify) ); + + return params; + } + + fn identify( + &mut self, + handle: &mut LinkHandle, + id: identify::Reader, + ) -> Result<()> { + let id = id.get_key(); + + let mut chat_message = MsgBuffer::::new(); + chat_message.build(|b| { + b.set_key(id); + }); + + handle.send_message(chat_message).display(); + Ok(()) + } + + fn close_handler( + &mut self, + handle: &mut LinkHandle, + _: terminate_stream::Reader, + ) -> Result<()> + { + // also close our end of the stream + handle.close_link()?; + return Ok(()); + } +} + +struct HostLink; +impl HostLink { + fn params(remote_id: ReactorId) -> LinkParams { + let mut params = LinkParams::new(remote_id, HostLink); + + params.external_handler( + host_message::Owned, + CtxHandler::new(Self::receive_host_message), + ); + + params.internal_handler( + bot_return::Owned, + CtxHandler::new(Self::send_chat_message), + ); + + params.external_handler( + client_kicked::Owned, + CtxHandler::new(Self::client_kicked), + ); + + return params; + } + + // pick up a 'send_message' event from the reactor, and put it to effect + // by constructing the chat message and sending it to the chat server. + fn send_chat_message( + &mut self, + handle: &mut LinkHandle, + send_message: bot_return::Reader, + ) -> Result<()> + { + let message = send_message.get_message()?; + + println!("Our bot sent"); + println!("{}", str::from_utf8(&message).unwrap()); + + let mut chat_message = MsgBuffer::::new(); + chat_message.build(|b| { + b.set_data(message); + }); + + handle.send_message(chat_message)?; + + return Ok(()); + } + + // pick up a 'send_message' event from the reactor, and put it to effect + // by constructing the chat message and sending it to the chat server. + fn client_kicked( + &mut self, + handle: &mut LinkHandle, + _: client_kicked::Reader, + ) -> Result<()> + { + // Disconnect + + handle.close_link()?; + + return Ok(()); + } + + // receive a chat message from the chat server, and broadcast it on the + // reactor. + fn receive_host_message( + &mut self, + handle: &mut LinkHandle, + host_message: host_message::Reader, + ) -> Result<()> + { + let message = host_message.get_data()?; + + let message: types::ServerMessage = serde_json::from_slice(message).unwrap(); + + println!(""); + match message { + types::ServerMessage::GameState(state) => { + // println!("New game state"); + let mut bot_msg = MsgBuffer::::new(); + + bot_msg.build(|b| { + b.set_input(&serde_json::to_vec(&state).unwrap()); + }); + handle.send_internal(bot_msg).display(); + }, + types::ServerMessage::FinalState(state) => { + println!("Game finished with"); + println!("{:?}", state); + } + types::ServerMessage::PlayerAction(action) => { + println!("Out bot did"); + println!("{:?}", action); + } + } + + return Ok(()); + } +} + +struct BotLink; +impl BotLink { + fn params(foreign_id: ReactorId) -> LinkParams { + let mut params = LinkParams::new(foreign_id, Self); + + params.external_handler(bot_return::Owned, CtxHandler::new(bot_return::e_to_i)); + params.internal_handler(bot_input::Owned, CtxHandler::new(bot_input::i_to_e)); + + return params; + } +} + +struct RuntimeLink; +impl RuntimeLink { + fn params(foreign_id: ReactorId) -> LinkParams { + let mut params = LinkParams::new(foreign_id, Self); + + params.external_handler( + actor_joined::Owned, + CtxHandler::new(actor_joined::e_to_i), + ); + + return params; + } } diff --git a/frontend/www/index.js b/frontend/www/index.js index a7c06fa..dcc5aa1 100644 --- a/frontend/www/index.js +++ b/frontend/www/index.js @@ -1,5 +1,6 @@ import { Game } from "planetwars"; import { memory } from "planetwars/plantwars_bg" +import { Shader } from "./webgl/shader" const URL = window.location.origin+window.location.pathname; const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1); diff --git a/frontend/www/package-lock.json b/frontend/www/package-lock.json index e681255..19fdad9 100644 --- a/frontend/www/package-lock.json +++ b/frontend/www/package-lock.json @@ -4063,6 +4063,12 @@ "sha.js": "^2.4.8" } }, + "picomatch": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz", + "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==", + "dev": true + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -4094,8 +4100,7 @@ } }, "planetwars": { - "version": "file:../pkg", - "dev": true + "version": "file:../pkg" }, "portfinder": { "version": "1.0.21", @@ -5192,6 +5197,70 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "dev": true }, + "ts-loader": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-6.1.0.tgz", + "integrity": "sha512-7JedeOu2rsYHQDEr2fwmMozABwbQTZXEaEMZPSIWG7gpzRefOLJCqwdazcegHtyaxp04PeEgs/b0m08WMpnIzQ==", + "dev": true, + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", @@ -5220,6 +5289,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", + "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", + "dev": true + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", diff --git a/frontend/www/package.json b/frontend/www/package.json index e287e37..46e25a6 100644 --- a/frontend/www/package.json +++ b/frontend/www/package.json @@ -2,7 +2,8 @@ "name": "create-wasm-app", "version": "0.1.0", "description": "create an app to consume rust-generated wasm packages", - "main": "index.js", + "main": "dist/index.js", + "types": "dist/index.d.ts", "scripts": { "build": "webpack --config webpack.config.js", "start": "webpack-dev-server" @@ -18,6 +19,9 @@ "webpack", "mozaic" ], + "dependencies": { + "planetwars": "file:../pkg" + }, "author": "Arthur Vercruysse ", "license": "(MIT OR Apache-2.0)", "bugs": { @@ -25,8 +29,9 @@ }, "homepage": "https://github.com/ajuvercr/Planetwars#Readme", "devDependencies": { - "planetwars": "file:../pkg", "webpack": "^4.29.3", + "ts-loader": "^6.0.2", + "typescript": "^3.5.2", "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.1.5", "copy-webpack-plugin": "^5.0.0" diff --git a/frontend/www/tsconfig.json b/frontend/www/tsconfig.json new file mode 100644 index 0000000..f5c8a2e --- /dev/null +++ b/frontend/www/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "lib": ["es2017", "es7", "es6", "dom"], + "outDir": "./dist/", + "noImplicitAny": false, + "module": "commonjs", + "target": "es6", + "jsx": "react", + "declaration": true + }, + "exclude": [ + "node_modules", + "dist" + ] + } diff --git a/frontend/www/webgl/buffer.ts b/frontend/www/webgl/buffer.ts new file mode 100644 index 0000000..2739fbe --- /dev/null +++ b/frontend/www/webgl/buffer.ts @@ -0,0 +1,55 @@ + +export class Buffer { + buffer: WebGLBuffer; + data: any; + count: number; + type: number; + + constructor(gl: WebGLRenderingContext, data: number[], type: number) { + this.buffer = gl.createBuffer(); + this.type = type; + + if (data) + this.updateData(gl, data); + } + + _toArray(data: number[]): any { + return new Float32Array(data); + } + + updateData(gl: WebGLRenderingContext, data: number[]) { + this.data = data; + this.count = data.length; + gl.bindBuffer(this.type, this.buffer); + gl.bufferData(this.type, this._toArray(data), gl.STATIC_DRAW); + } + + bind(gl: WebGLRenderingContext) { + gl.bindBuffer(this.type, this.buffer); + } + + getCount(): number { + return this.count; + } +} + +export class VertexBuffer extends Buffer { + constructor(gl: WebGLRenderingContext, data: any) { + super(gl, data, gl.ARRAY_BUFFER); + } + + _toArray(data: number[]): any { + return new Float32Array(data); + } +} + + +export class IndexBuffer extends Buffer { + constructor(gl: WebGLRenderingContext, data: any) { + super(gl, data, gl.ELEMENT_ARRAY_BUFFER); + } + + _toArray(data: number[]): any { + return new Uint16Array(data); + } +} diff --git a/frontend/www/webgl/instance.ts b/frontend/www/webgl/instance.ts new file mode 100644 index 0000000..ec85144 --- /dev/null +++ b/frontend/www/webgl/instance.ts @@ -0,0 +1,72 @@ +import { Renderable } from './renderer'; +import { Shader, Uniform } from './shader'; +import { Dictionary } from './util'; + +function createAndSetupTexture(gl: WebGLRenderingContext): WebGLTexture { + var texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + + // Set up texture so we can render any size image and so we are + // working with pixels. + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + return texture; +} + +export class Foo implements Renderable { + stages: Stage[]; + + textures: WebGLTexture[]; + framebuffers: WebGLFramebuffer[]; + + width: number; + height: number; + + constructor(gl: WebGLRenderingContext, width: number, height: number) { + this.width = width; + this.height = height; + + for (let ii = 0; ii < 2; ++ii) { + const texture = createAndSetupTexture(gl); + this.textures.push(texture); + + // make the texture the same size as the image + gl.texImage2D( + gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, + gl.RGBA, gl.UNSIGNED_BYTE, null); + + // Create a framebuffer + const fbo = gl.createFramebuffer(); + this.framebuffers.push(fbo); + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); + + // Attach a texture to it. + gl.framebufferTexture2D( + gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); + } + } + + render(gl: WebGLRenderingContext) { + this.stages.forEach( (item, i) => { + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffers[i%2]); + item.render(gl); + gl.bindTexture(gl.TEXTURE_2D, this.textures[i % 2]); + }); + } +} + +class Stage implements Renderable { + program: Shader; + uniforms: Dictionary; + + render(gl: WebGLRenderingContext) { + this.program.bind(gl); + + for (let name in this.uniforms) { + this.program.uniform(gl, name, this.uniforms[name]); + } + } +} diff --git a/frontend/www/webgl/renderer.ts b/frontend/www/webgl/renderer.ts new file mode 100644 index 0000000..2c4f6fa --- /dev/null +++ b/frontend/www/webgl/renderer.ts @@ -0,0 +1,72 @@ + +import { IndexBuffer } from './buffer'; +import { Shader, Uniform1i } from './shader'; +import { VertexArray } from './vertexBufferLayout'; +import { Texture } from './texture'; + +export interface Renderable { + render(gl: WebGLRenderingContext): void; +} + +export class Renderer { + renderables: Renderable[]; + + indexBuffers: IndexBuffer[]; + vertexArrays: VertexArray[]; + shaders: Shader[]; + textures: Texture[]; + + constructor() { + this.indexBuffers = []; + this.vertexArrays = []; + this.shaders = []; + this.textures = []; + } + + addRenderable(item: Renderable) { + this.renderables.push(item); + } + + addToDraw(indexBuffer: IndexBuffer, vertexArray: VertexArray, shader: Shader, texture?: Texture): number { + this.indexBuffers.push(indexBuffer); + this.vertexArrays.push(vertexArray); + this.shaders.push(shader); + this.textures.push(texture); + + return this.indexBuffers.length - 1; + } + + render(gl: WebGLRenderingContext) { + const maxTextures = gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS); + let texLocation = 0; + + for(let i = 0; i < this.indexBuffers.length; i ++) { + const indexBuffer = this.indexBuffers[i]; + const vertexArray = this.vertexArrays[i]; + const shader = this.shaders[i]; + const texture = this.textures[i]; + + if (texture) { + + shader.uniform(gl, texture.name, new Uniform1i(texLocation)); + texture.bind(gl, texLocation); + + texLocation ++; + if (texLocation > maxTextures) { + console.error("Using too many textures, this is not supported yet\nUndefined behaviour!"); + } + } + + if (vertexArray && shader) { + vertexArray.bind(gl, shader); + + if (indexBuffer) { + indexBuffer.bind(gl); + gl.drawElements(gl.TRIANGLES, indexBuffer.getCount(), gl.UNSIGNED_SHORT, 0); + } else { + console.error("IndexBuffer is required to render, for now"); + } + } + } + } +} diff --git a/frontend/www/webgl/shader.ts b/frontend/www/webgl/shader.ts new file mode 100644 index 0000000..409d81a --- /dev/null +++ b/frontend/www/webgl/shader.ts @@ -0,0 +1,305 @@ +import { Dictionary } from './util'; + +function error(msg: string) { + console.log(msg); +} + +const defaultShaderType = [ + "VERTEX_SHADER", + "FRAGMENT_SHADER" +]; + +function loadShader( + gl: WebGLRenderingContext, + shaderSource: string, + shaderType: number, + opt_errorCallback: any, +): WebGLShader { + var errFn = opt_errorCallback || error; + // Create the shader object + var shader = gl.createShader(shaderType); + + // Load the shader source + gl.shaderSource(shader, shaderSource); + + // Compile the shader + gl.compileShader(shader); + + // Check the compile status + var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS); + if (!compiled) { + // Something went wrong during compilation; get the error + var lastError = gl.getShaderInfoLog(shader); + errFn("*** Error compiling shader '" + shader + "':" + lastError); + gl.deleteShader(shader); + return null; + } + + console.log("created shader with source"); + console.log(shaderSource); + + return shader; +} + +function createProgram( + gl: WebGLRenderingContext, + shaders: WebGLShader[], + opt_attribs: string[], + opt_locations: number[], + opt_errorCallback: any, +): WebGLProgram { + var errFn = opt_errorCallback || error; + var program = gl.createProgram(); + shaders.forEach(function (shader) { + gl.attachShader(program, shader); + }); + if (opt_attribs) { + opt_attribs.forEach(function (attrib, ndx) { + gl.bindAttribLocation( + program, + opt_locations ? opt_locations[ndx] : ndx, + attrib); + }); + } + gl.linkProgram(program); + + // Check the link status + var linked = gl.getProgramParameter(program, gl.LINK_STATUS); + if (!linked) { + // something went wrong with the link + var lastError = gl.getProgramInfoLog(program); + errFn("Error in program linking:" + lastError); + + gl.deleteProgram(program); + return null; + } + return program; +} + +function createShaderFromScript( + gl: WebGLRenderingContext, + scriptId: string, + context: Dictionary, + opt_shaderType: number, + opt_errorCallback: any, +): WebGLShader { + var shaderSource = ""; + var shaderType; + var shaderScript = document.getElementById(scriptId) as HTMLScriptElement; + if (!shaderScript) { + console.log("*** Error: unknown script element" + scriptId); + } + shaderSource = shaderScript.text; + + for (let key in context) { + console.log("substitute " + key); + shaderSource = shaderSource.replace(new RegExp("\\$" + key, 'g'), context[key]); + } + + if (!opt_shaderType) { + if (shaderScript.type === "x-shader/x-vertex") { + shaderType = 35633; + } else if (shaderScript.type === "x-shader/x-fragment") { + shaderType = 35632; + } else if (shaderType !== gl.VERTEX_SHADER && shaderType !== gl.FRAGMENT_SHADER) { + console.log("*** Error: unknown shader type"); + } + } + + return loadShader( + gl, shaderSource, opt_shaderType ? opt_shaderType : shaderType, + opt_errorCallback); +} + +export class Shader { + shader: WebGLProgram; + uniformCache: Dictionary; + attribCache: Dictionary; + + static createProgramFromScripts( + gl: WebGLRenderingContext, + shaderScriptIds: string[], + context = {}, + opt_attribs?: string[], + opt_locations?: number[], + opt_errorCallback?: any, + ): Shader { + var shaders = []; + for (var ii = 0; ii < shaderScriptIds.length; ++ii) { + shaders.push(createShaderFromScript( + gl, shaderScriptIds[ii], context, (gl as any)[defaultShaderType[ii % 2]] as number, opt_errorCallback)); + } + return new Shader(createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback)); + } + + static async createProgramFromUrls( + gl: WebGLRenderingContext, + vert_url: string, + frag_url: string, + context?: Dictionary, + opt_attribs?: string[], + opt_locations?: number[], + opt_errorCallback?: any, + ): Promise { + const sources = (await Promise.all([ + fetch(vert_url).then((r) => r.text()), + fetch(frag_url).then((r) => r.text()), + ])).map(x => { + for (let key in context) { + x = x.replace(new RegExp("\\$" + key, 'g'), context[key]); + } + return x; + }); + + const shaders = [ + loadShader(gl, sources[0], 35633, opt_errorCallback), + loadShader(gl, sources[1], 35632, opt_errorCallback), + ]; + return new Shader(createProgram(gl, shaders, opt_attribs, opt_locations, opt_errorCallback)); + } + + constructor(shader: WebGLProgram) { + this.shader = shader; + this.uniformCache = {}; + this.attribCache = {}; + } + + bind(gl: WebGLRenderingContext) { + gl.useProgram(this.shader); + } + + // Different locations have different types :/ + getUniformLocation(gl: WebGLRenderingContext, name: string): WebGLUniformLocation { + if (this.uniformCache[name] === undefined) { + this.uniformCache[name] = gl.getUniformLocation(this.shader, name); + } + + return this.uniformCache[name]; + } + + getAttribLocation(gl: WebGLRenderingContext, name: string): number { + if (this.attribCache[name] === undefined) { + this.attribCache[name] = gl.getAttribLocation(this.shader, name); + } + + return this.attribCache[name]; + } + + uniform( + gl: WebGLRenderingContext, + name: string, + uniform: T, + ) { + this.bind(gl); + const location = this.getUniformLocation(gl, name); + if (location < 0) { + console.log("No location found with name " + name); + } + + uniform.setUniform(gl, location); + } +} + +export interface Uniform { + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation): void; +} + +export class Uniform2fv implements Uniform { + data: number[]; + constructor(data: number[]) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform2fv(location, this.data); + } +} + +export class Uniform3fv implements Uniform { + data: number[]; + constructor(data: number[]) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform3fv(location, this.data); + } +} + +export class Uniform1iv implements Uniform { + data: number[]; + constructor(data: number[]) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform1iv(location, this.data); + } +} + +export class Uniform1i implements Uniform { + texture: number; + + constructor(texture: number) { + this.texture = texture; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform1i(location, this.texture); + } +} + +export class Uniform1f implements Uniform { + texture: number; + + constructor(texture: number) { + this.texture = texture; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform1f(location, this.texture); + } +} + +export class Uniform2f implements Uniform { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform2f(location, this.x, this.y); + } +} + +export class Uniform4f implements Uniform { + v0: number; + v1: number; + v2: number; + v3: number; + + constructor(vec: number[]) { + this.v0 = vec[0]; + this.v1 = vec[1]; + this.v2 = vec[2]; + this.v3 = vec[3]; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniform4f(location, this.v0, this.v1, this.v2, this.v3); + } +} + +export class UniformMatrix3fv implements Uniform { + data: number[]; + constructor(data: number[]) { + this.data = data; + } + + setUniform(gl: WebGLRenderingContext, location: WebGLUniformLocation) { + gl.uniformMatrix3fv(location, false, this.data); + } +} diff --git a/frontend/www/webgl/texture.ts b/frontend/www/webgl/texture.ts new file mode 100644 index 0000000..a9cbb39 --- /dev/null +++ b/frontend/www/webgl/texture.ts @@ -0,0 +1,68 @@ + + +// TODO: fix texture locations, not use only 0 +export class Texture { + texture: WebGLTexture; + image: HTMLImageElement; + loaded: boolean; + name: string; + + constructor( + gl: WebGLRenderingContext, + path: string, + name: string, + ) { + this.loaded = false; + this.name = name; + + this.image = new Image(); + this.image.onload = () => this.handleImageLoaded(gl); + this.image.onerror = error; + this.image.src = path; + + this.texture = gl.createTexture(); + this.bind(gl); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, + gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255])); + } + + handleImageLoaded(gl: WebGLRenderingContext) { + console.log('handling image loaded'); + this.bind(gl); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.image); + + this.unbind(gl); + + this.loaded = true; + } + + bind(gl: WebGLRenderingContext, location=0) { + gl.activeTexture(gl.TEXTURE0 + location); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + } + + unbind(gl: WebGLRenderingContext) { + gl.bindTexture(gl.TEXTURE_2D, null); + } + + + getWidth(): number { + return this.image.width; + } + + getHeight(): number { + return this.image.height; + } +} + +function error(e: any) { + console.error("IMAGE LOAD ERROR"); + console.error(e); +} diff --git a/frontend/www/webgl/util.ts b/frontend/www/webgl/util.ts new file mode 100644 index 0000000..7aa4a6c --- /dev/null +++ b/frontend/www/webgl/util.ts @@ -0,0 +1,129 @@ + +export interface Dictionary { + [Key: string]: T; +} + + +interface OnLoadable { + onload: any; +} + +export function onload2promise(obj: T): Promise { + return new Promise(resolve => { + obj.onload = () => resolve(obj); + }); +} + +export function resizeCanvasToDisplaySize( + canvas: HTMLCanvasElement, + multiplier?: number, +): boolean { + multiplier = multiplier || 1; + var width = canvas.clientWidth * multiplier | 0; + var height = canvas.clientHeight * multiplier | 0; + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + return true; + } + return false; +} + +export class FPSCounter { + last: number; + count: number; + constructor() { + this.last = 0; + this.count = 0; + } + + frame(now: number) { + this.count += 1; + if (now - this.last > 1) { + this.last = now; + console.log(this.count + " fps"); + this.count = 0; + } + } +} + +export class M3 { + _data: any; + + constructor(data: any) { + this._data = data; + } + + static ident(): M3 { + return new M3([ + 1, 0, 0, + 0, 1, 0, + 0, 0, 1 + ]); + } + + multiply(other: M3): M3 { + const a = this._data; + const b = other._data; + + var a00 = a[0 * 3 + 0]; + var a01 = a[0 * 3 + 1]; + var a02 = a[0 * 3 + 2]; + var a10 = a[1 * 3 + 0]; + var a11 = a[1 * 3 + 1]; + var a12 = a[1 * 3 + 2]; + var a20 = a[2 * 3 + 0]; + var a21 = a[2 * 3 + 1]; + var a22 = a[2 * 3 + 2]; + var b00 = b[0 * 3 + 0]; + var b01 = b[0 * 3 + 1]; + var b02 = b[0 * 3 + 2]; + var b10 = b[1 * 3 + 0]; + var b11 = b[1 * 3 + 1]; + var b12 = b[1 * 3 + 2]; + var b20 = b[2 * 3 + 0]; + var b21 = b[2 * 3 + 1]; + var b22 = b[2 * 3 + 2]; + + return new M3([ + b00 * a00 + b01 * a10 + b02 * a20, + b00 * a01 + b01 * a11 + b02 * a21, + b00 * a02 + b01 * a12 + b02 * a22, + b10 * a00 + b11 * a10 + b12 * a20, + b10 * a01 + b11 * a11 + b12 * a21, + b10 * a02 + b11 * a12 + b12 * a22, + b20 * a00 + b21 * a10 + b22 * a20, + b20 * a01 + b21 * a11 + b22 * a21, + b20 * a02 + b21 * a12 + b22 * a22, + ]); + } + + translation(x: number, y: number): M3 { + const out = [...this._data]; + out[6] += x; + out[7] += y; + return new M3(out); + } + + rotate(rad: number): M3 { + var c = Math.cos(rad); + var s = Math.sin(rad); + + const out = new M3([...this._data]); + + return out.multiply(new M3([ + c, -s, 0, + s, c, 0, + 0, 0, 1 + ])); + } + + scale(s_x: number, s_y = s_x, s_z = 1): M3 { + const out = new M3([...this._data]); + return out.multiply(new M3([ + s_x, 0, 0, + 0, s_y, 0, + 0, 0, s_z, + ])); + } +} diff --git a/frontend/www/webgl/vertexArray.ts b/frontend/www/webgl/vertexArray.ts new file mode 100644 index 0000000..e69de29 diff --git a/frontend/www/webgl/vertexBufferLayout.ts b/frontend/www/webgl/vertexBufferLayout.ts new file mode 100644 index 0000000..22fd868 --- /dev/null +++ b/frontend/www/webgl/vertexBufferLayout.ts @@ -0,0 +1,112 @@ +import { Buffer, VertexBuffer } from './buffer'; +import { Shader } from './shader'; + +export class VertexBufferElement { + type: number; + amount: number; + type_size: number; + normalized: boolean; + index: string; + + constructor( + type: number, + amount: number, + type_size: number, + index: string, + normalized: boolean, + ) { + this.type = type; + this.amount = amount; + this.type_size = type_size; + this.normalized = normalized; + this.index = index; + } +} + +export class VertexBufferLayout { + elements: VertexBufferElement[]; + stride: number; + offset: number; + + constructor(offset = 0) { + this.elements = []; + this.stride = 0; + this.offset = offset; + } + + // Maybe wrong normalized type + push( + type: number, + amount: number, + type_size: number, + index: string, + normalized = false, + ) { + this.elements.push(new VertexBufferElement(type, amount, type_size, index, normalized)); + this.stride += amount * type_size; + } + + getElements(): VertexBufferElement[] { + return this.elements; + } + + getStride(): number { + return this.stride; + } +} + +// glEnableVertexAttribArray is to specify what location of the current program the follow data is needed +// glVertexAttribPointer tells gl that that data is at which location in the supplied data +export class VertexArray { + // There is no renderer ID, always at bind buffers and use glVertexAttribPointer + buffers: Buffer[]; + layouts: VertexBufferLayout[]; + + constructor() { + this.buffers = []; + this.layouts = []; + } + + addBuffer(vb: VertexBuffer, layout: VertexBufferLayout) { + this.buffers.push(vb); + this.layouts.push(layout); + } + + /// Bind buffers providing program data + bind(gl: WebGLRenderingContext, shader: Shader) { + shader.bind(gl); + for(let i = 0; i < this.buffers.length; i ++) { + const buffer = this.buffers[i]; + const layout = this.layouts[i]; + + buffer.bind(gl); + const elements = layout.getElements(); + let offset = layout.offset; + + for (let j = 0; j < elements.length; j ++) { + const element = elements[j]; + const location = shader.getAttribLocation(gl, element.index); + + if (location >= 0) { + gl.enableVertexAttribArray(location); + gl.vertexAttribPointer( + location, element.amount, element.type, + element.normalized, layout.stride, offset + ); + } + + offset += element.amount * element.type_size; + } + } + } + + /// Undo bind operation + unbind(gl: WebGLRenderingContext) { + this.layouts.forEach((layout) => { + layout.getElements().forEach((_, index) => { + gl.disableVertexAttribArray(index); + }); + }) + } +} + diff --git a/frontend/www/webpack.config.js b/frontend/www/webpack.config.js index 80ad814..1b0d636 100644 --- a/frontend/www/webpack.config.js +++ b/frontend/www/webpack.config.js @@ -2,12 +2,24 @@ const CopyWebpackPlugin = require("copy-webpack-plugin"); const path = require('path'); module.exports = { - entry: "./bootstrap.js", - output: { - path: path.resolve(__dirname, "dist"), - filename: "bootstrap.js", + mode: 'development', + entry: './bootstrap.js', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + } + ] + }, + resolve: { + extensions: [ '.tsx', '.ts', '.js', '.wasm' ] + }, + output: { + filename: 'bootstrap.js', + path: path.resolve(__dirname, 'dist') }, - mode: "development", plugins: [ new CopyWebpackPlugin(['index.html']) ], From cd3e42beaa35795873d58d520c0fe80733d1b45b Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Mon, 16 Sep 2019 21:18:35 +0200 Subject: [PATCH 6/8] add types --- client/bot7.txt | 23 +++++++++++++++ client/log.log | 0 client/run.sh | 4 +++ client/simple.py | 29 +++++++++++++++++++ client/src/types.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 125 insertions(+) create mode 100644 client/bot7.txt create mode 100644 client/log.log create mode 100755 client/run.sh create mode 100644 client/simple.py create mode 100644 client/src/types.rs diff --git a/client/bot7.txt b/client/bot7.txt new file mode 100644 index 0000000..ca4bcd4 --- /dev/null +++ b/client/bot7.txt @@ -0,0 +1,23 @@ +start{"planets":[{"ship_count":7,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":7,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":0,"ship_count":6,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":5},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":10}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":0,"ship_count":6,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":4},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":9},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":11},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":11}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":0,"ship_count":6,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":3},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":8},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":10},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":10},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":11},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":11}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":0,"ship_count":6,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":2},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":7},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":9},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":9},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":10},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":10},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":11},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":11}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":0,"ship_count":6,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":1},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":6},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":8},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":8},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":9},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":9},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":10},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":10},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":11},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":11}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":11},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":5},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":7},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":7},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":8},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":8},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":9},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":9},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":10},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":10},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":11}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":10},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":4},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":6},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":6},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":7},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":7},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":8},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":8},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":9},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":9},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":10},{"id":12,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":5},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":10}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":9},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":3},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":5},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":5},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":6},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":6},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":7},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":7},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":8},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":8},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":9},{"id":12,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":4},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":9},{"id":14,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":5},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":10}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":8},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":2},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":4},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":4},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":5},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":5},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":6},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":6},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":7},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":7},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":8},{"id":12,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":3},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":8},{"id":14,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":4},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":9},{"id":16,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":5},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":10}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":7},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":1},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":3},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":3},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":4},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":4},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":5},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":5},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":6},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":6},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":7},{"id":12,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":2},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":7},{"id":14,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":3},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":8},{"id":16,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":4},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":9},{"id":18,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":5},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":10}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":6},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":10},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":2},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":2},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":3},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":3},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":4},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":4},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":5},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":5},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":6},{"id":12,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":1},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":6},{"id":14,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":2},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":7},{"id":16,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":3},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":8},{"id":18,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":4},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":9},{"id":20,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":5},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":9},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":1},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":1},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":2},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":2},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":3},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":3},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":4},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":4},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":5},{"id":23,"ship_count":5,"origin":"duteros","destination":"protos","owner":1,"turns_remaining":5},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":5},{"id":14,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":1},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":6},{"id":16,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":2},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":7},{"id":18,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":3},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":8},{"id":20,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":4},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":11}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":4},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":8},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":11},{"id":24,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":5},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":1},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":1},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":2},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":2},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":3},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":3},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":4},{"id":23,"ship_count":5,"origin":"duteros","destination":"protos","owner":1,"turns_remaining":4},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":4},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":10},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":5},{"id":16,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":1},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":6},{"id":18,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":2},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":7},{"id":20,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":3}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":3},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":7},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":10},{"id":24,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":4},{"id":20,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":2},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":6},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":1},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":1},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":2},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":2},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":3},{"id":23,"ship_count":5,"origin":"duteros","destination":"protos","owner":1,"turns_remaining":3},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":3},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":9},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":4},{"id":18,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":1},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":2},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":6},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":9},{"id":24,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":3},{"id":20,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":1},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":5},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":4},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":3},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":1},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":1},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":2},{"id":23,"ship_count":5,"origin":"duteros","destination":"protos","owner":1,"turns_remaining":2},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":2},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":8}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":1},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":5},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":8},{"id":24,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":2},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":7},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":4},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":3},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":2},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":1},{"id":23,"ship_count":5,"origin":"duteros","destination":"protos","owner":1,"turns_remaining":1},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":1}]} +{"planets":[{"ship_count":4,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":1},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":4},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":7},{"id":24,"ship_count":1,"origin":"protos","destination":"duteros","owner":2,"turns_remaining":1},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":6},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":3},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":2}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":1,"turns_remaining":10},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":3},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":6},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":1},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":5},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":2}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":1,"turns_remaining":9},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":2},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":5},{"id":27,"ship_count":3,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":5},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":4},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":4,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":1,"turns_remaining":8},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":1,"turns_remaining":1},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":4},{"id":27,"ship_count":3,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":4},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":3},{"id":28,"ship_count":2,"origin":"protos","destination":"tritos","owner":1,"turns_remaining":10}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":5,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":1,"turns_remaining":7},{"id":29,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":5},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":3},{"id":27,"ship_count":3,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":3},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":2},{"id":28,"ship_count":2,"origin":"protos","destination":"tritos","owner":1,"turns_remaining":9}]} +{"planets":[{"ship_count":4,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":1,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":1,"turns_remaining":6},{"id":29,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":4},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":1,"turns_remaining":2},{"id":27,"ship_count":3,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":2},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":2,"turns_remaining":1},{"id":28,"ship_count":2,"origin":"protos","destination":"tritos","owner":1,"turns_remaining":8},{"id":30,"ship_count":4,"origin":"tetartos","destination":"tritos","owner":1,"turns_remaining":5}]} diff --git a/client/log.log b/client/log.log new file mode 100644 index 0000000..e69de29 diff --git a/client/run.sh b/client/run.sh new file mode 100755 index 0000000..6436fed --- /dev/null +++ b/client/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +rm bot*.txt +cargo run $1 python simple.py diff --git a/client/simple.py b/client/simple.py new file mode 100644 index 0000000..02d6fc4 --- /dev/null +++ b/client/simple.py @@ -0,0 +1,29 @@ +import sys, json, random + +def move(command): + record = { 'moves': [command] } + print(json.dumps(record)) + sys.stdout.flush() + +f_name = f"bot{random.randint(0, 10)}.txt" +f = open(f_name,"w+") +f.write("start") +f.flush() +for line in sys.stdin: + f.write(line) + f.flush() + state = json.loads(line) + # find planet with most ships + my_planets = [p for p in state['planets'] if p['owner'] == 1] + other_planets = [p for p in state['planets'] if p['owner'] != 1] + + if not my_planets or not other_planets: + move(None) + else: + planet = max(my_planets, key=lambda p: p['ship_count']) + dest = min(other_planets, key=lambda p: p['ship_count']) + move({ + 'origin': planet['name'], + 'destination': dest['name'], + 'ship_count': planet['ship_count'] - 1 + }) diff --git a/client/src/types.rs b/client/src/types.rs new file mode 100644 index 0000000..fd580b3 --- /dev/null +++ b/client/src/types.rs @@ -0,0 +1,69 @@ +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type", content = "content")] +pub enum ServerMessage { + /// Game state in current turn + GameState(State), + /// The action that was performed + PlayerAction(PlayerAction), + /// The game is over, and this is the concluding state. + FinalState(State), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[serde(tag = "type", content = "value")] +pub enum PlayerAction { + Timeout, + ParseError(String), + Commands(Vec), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PlayerCommand { + pub command: Command, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum CommandError { + NotEnoughShips, + OriginNotOwned, + ZeroShipMove, + OriginDoesNotExist, + DestinationDoesNotExist, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Command { + pub origin: String, + pub destination: String, + pub ship_count: u64, +} + + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct State { + pub planets: Vec, + pub expeditions: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Expedition { + pub id: u64, + pub ship_count: u64, + pub origin: String, + pub destination: String, + pub owner: usize, + pub turns_remaining: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Planet { + pub ship_count: u64, + pub x: f64, + pub y: f64, + pub owner: Option, + pub name: String, +} From 2bd6d3a6d7f6d814046e099520dae05c1733ba33 Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Mon, 16 Sep 2019 21:18:52 +0200 Subject: [PATCH 7/8] add maps --- backend/game.json | 24 +++++++++++ backend/hex.json | 43 ++++++++++++++++++++ backend/standard_game.json | 82 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 backend/game.json create mode 100644 backend/hex.json create mode 100644 backend/standard_game.json diff --git a/backend/game.json b/backend/game.json new file mode 100644 index 0000000..91031be --- /dev/null +++ b/backend/game.json @@ -0,0 +1,24 @@ +{"planets":[{"ship_count":7,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":7,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":0,"ship_count":6,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":10}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":0,"ship_count":6,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":9},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":11},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":11}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":0,"ship_count":6,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":8},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":10},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":10},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":11},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":11}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":0,"ship_count":6,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":7},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":9},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":9},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":10},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":10},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":11},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":11}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":0,"ship_count":6,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":6},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":8},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":8},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":9},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":9},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":10},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":10},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":11},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":11}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":11},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":5},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":7},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":7},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":8},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":8},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":9},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":9},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":10},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":10},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":11}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":10},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":4},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":6},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":6},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":7},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":7},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":8},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":8},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":9},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":9},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":10},{"id":12,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":10}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":9},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":3},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":5},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":5},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":6},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":6},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":7},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":7},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":8},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":8},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":9},{"id":12,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":9},{"id":14,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":10}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":8},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":2},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":4},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":4},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":5},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":5},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":6},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":6},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":7},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":7},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":8},{"id":12,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":8},{"id":14,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":9},{"id":16,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":10}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":7},{"id":1,"ship_count":6,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":1},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":3},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":3},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":4},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":4},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":5},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":5},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":6},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":6},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":7},{"id":12,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":7},{"id":14,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":8},{"id":16,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":9},{"id":18,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":10}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":6},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":10},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":2},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":2},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":3},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":3},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":4},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":4},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":5},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":5},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":6},{"id":12,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":6},{"id":14,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":7},{"id":16,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":8},{"id":18,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":9},{"id":20,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":5},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":9},{"id":2,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":1},{"id":3,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":1},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":2},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":2},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":3},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":3},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":4},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":4},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":5},{"id":23,"ship_count":5,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":5},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":5},{"id":14,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":6},{"id":16,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":7},{"id":18,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":8},{"id":20,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":11}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":4},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":8},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":11},{"id":24,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":4,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":1},{"id":5,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":1},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":2},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":2},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":3},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":3},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":4},{"id":23,"ship_count":5,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":4},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":4},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":10},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":5},{"id":16,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":6},{"id":18,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":7},{"id":20,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":3},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":7},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":10},{"id":24,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":20,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":6},{"id":6,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":1},{"id":7,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":1},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":2},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":2},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":3},{"id":23,"ship_count":5,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":3},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":3},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":9},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":4},{"id":18,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":2},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":6},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":9},{"id":24,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":20,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":5},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":4},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":3},{"id":8,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":1},{"id":9,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":1},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":2},{"id":23,"ship_count":5,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":2},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":2},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":8}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":11,"ship_count":1,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":1},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":5},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":8},{"id":24,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":7},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":4},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":3},{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":2},{"id":13,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":1},{"id":23,"ship_count":5,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":1},{"id":10,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":4,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":15,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":1},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":4},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":7},{"id":24,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":6},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":3},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":2}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":10},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":3},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":6},{"id":17,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":1},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":5},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":2}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":9},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":2},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":5},{"id":27,"ship_count":3,"origin":"duteros","destination":"tritos","owner":2,"turns_remaining":5},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":4},{"id":19,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":1}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":4,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":8},{"id":21,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":1},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":4},{"id":27,"ship_count":3,"origin":"duteros","destination":"tritos","owner":2,"turns_remaining":4},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":3},{"id":28,"ship_count":2,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":10}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":5,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":7},{"id":29,"ship_count":4,"origin":"duteros","destination":"tritos","owner":2,"turns_remaining":5},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":3},{"id":27,"ship_count":3,"origin":"duteros","destination":"tritos","owner":2,"turns_remaining":3},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":2},{"id":28,"ship_count":2,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":9}]} +{"planets":[{"ship_count":4,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":6},{"id":29,"ship_count":4,"origin":"duteros","destination":"tritos","owner":2,"turns_remaining":4},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":2},{"id":27,"ship_count":3,"origin":"duteros","destination":"tritos","owner":2,"turns_remaining":2},{"id":22,"ship_count":1,"origin":"protos","destination":"tetartos","owner":1,"turns_remaining":1},{"id":28,"ship_count":2,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":8},{"id":30,"ship_count":4,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"}],"expeditions":[{"id":26,"ship_count":3,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":5},{"id":29,"ship_count":4,"origin":"duteros","destination":"tritos","owner":2,"turns_remaining":3},{"id":25,"ship_count":2,"origin":"tetartos","destination":"protos","owner":2,"turns_remaining":1},{"id":27,"ship_count":3,"origin":"duteros","destination":"tritos","owner":2,"turns_remaining":1},{"id":31,"ship_count":3,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":10},{"id":28,"ship_count":2,"origin":"protos","destination":"tritos","owner":2,"turns_remaining":7},{"id":30,"ship_count":4,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4}]} diff --git a/backend/hex.json b/backend/hex.json new file mode 100644 index 0000000..5ef4f31 --- /dev/null +++ b/backend/hex.json @@ -0,0 +1,43 @@ +{ + "planets": [ + { + "name": "protos", + "x": -6, + "y": 0, + "owner": 1, + "ship_count": 6 + }, + { + "name": "duteros", + "x": -3, + "y": 5, + "ship_count": 6 + }, + { + "name": "tritos", + "x": 3, + "y": 5, + "ship_count": 6 + }, + { + "name": "tetartos", + "x": 6, + "y": 0, + "owner": 2, + "ship_count": 6 + }, + { + "name": "pemptos", + "x": 3, + "y": -5, + "ship_count": 6 + }, + { + "name": "extos", + "x": -3, + "y": -5, + "ship_count": 6 + } + ] +} + diff --git a/backend/standard_game.json b/backend/standard_game.json new file mode 100644 index 0000000..69e74c6 --- /dev/null +++ b/backend/standard_game.json @@ -0,0 +1,82 @@ +{"players":["bot_auto_player_full_rand","adversary_auto_player_orfirst_rand"]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":0,"ship_count":3,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":1,"ship_count":3,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":0,"ship_count":3,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":1,"ship_count":3,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":0,"ship_count":3,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":1,"ship_count":3,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":2,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":3,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":0,"ship_count":3,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":1,"ship_count":3,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":2,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":3,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":4,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":5,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":0,"ship_count":3,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":1,"ship_count":3,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":2,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":3,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":4,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":5,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":6,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":7,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":6,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":2,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":3,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":4,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":5,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":4,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":7,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":6,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":2,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":3,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":4,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":5,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":8,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":0,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":7,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":6,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":10,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":9,"ship_count":4,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":10},{"id":4,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":5,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":8,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":7,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":6,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":10,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":9,"ship_count":4,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":9},{"id":12,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":11,"ship_count":3,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":5},{"id":8,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":7,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":14,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":10,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":9,"ship_count":4,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":8},{"id":12,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":11,"ship_count":3,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":4},{"id":8,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":13,"ship_count":3,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":16,"ship_count":3,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":14,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":10,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":9,"ship_count":4,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":7},{"id":12,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":11,"ship_count":3,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":3},{"id":8,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":13,"ship_count":3,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":15,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":16,"ship_count":3,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":14,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":10,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":9,"ship_count":4,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":6},{"id":12,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":11,"ship_count":3,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":2},{"id":18,"ship_count":2,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":5},{"id":13,"ship_count":3,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":15,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":17,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":4,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":16,"ship_count":3,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":14,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":20,"ship_count":4,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":5},{"id":9,"ship_count":4,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":5},{"id":12,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":11,"ship_count":3,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":1},{"id":18,"ship_count":2,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":4},{"id":13,"ship_count":3,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":15,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":17,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":19,"ship_count":5,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":16,"ship_count":3,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":14,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":20,"ship_count":4,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":4},{"id":9,"ship_count":4,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":4},{"id":22,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":21,"ship_count":1,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":5},{"id":18,"ship_count":2,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":3},{"id":13,"ship_count":3,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":15,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":17,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":19,"ship_count":5,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":4}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":4,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":16,"ship_count":3,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":24,"ship_count":3,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":20,"ship_count":4,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":3},{"id":9,"ship_count":4,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":3},{"id":22,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":21,"ship_count":1,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":4},{"id":18,"ship_count":2,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":2},{"id":23,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":15,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":17,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":19,"ship_count":5,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":3}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":7,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":26,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":24,"ship_count":3,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":20,"ship_count":4,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":2},{"id":9,"ship_count":4,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":2},{"id":22,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":21,"ship_count":1,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":3},{"id":18,"ship_count":2,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":1},{"id":23,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":25,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":10},{"id":17,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":19,"ship_count":5,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":2}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":5,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":4,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":26,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":24,"ship_count":3,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":20,"ship_count":4,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":1},{"id":9,"ship_count":4,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":1},{"id":22,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":21,"ship_count":1,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":2},{"id":28,"ship_count":3,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":5},{"id":23,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":25,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":9},{"id":27,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":19,"ship_count":5,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":26,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":24,"ship_count":3,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":30,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":29,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":22,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":21,"ship_count":1,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":1},{"id":28,"ship_count":3,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":4},{"id":23,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":25,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":8},{"id":27,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":26,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":24,"ship_count":3,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":30,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":29,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":32,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":31,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":28,"ship_count":3,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":3},{"id":23,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":25,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":7},{"id":27,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":4,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":26,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":34,"ship_count":4,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":5},{"id":30,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":29,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":32,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":31,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":28,"ship_count":3,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":2},{"id":33,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":25,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":6},{"id":27,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":5,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":36,"ship_count":6,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":5},{"id":34,"ship_count":4,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":4},{"id":30,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":29,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":32,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":31,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":28,"ship_count":3,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":1},{"id":33,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":25,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":5},{"id":27,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":35,"ship_count":2,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":36,"ship_count":6,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":4},{"id":34,"ship_count":4,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":3},{"id":30,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":29,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":32,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":31,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":38,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":33,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":25,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":4},{"id":37,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":35,"ship_count":2,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":4}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":5,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":4,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":36,"ship_count":6,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":3},{"id":34,"ship_count":4,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":2},{"id":40,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":39,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":32,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":31,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":38,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":33,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":25,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":3},{"id":37,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":35,"ship_count":2,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":3}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":36,"ship_count":6,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":2},{"id":34,"ship_count":4,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":1},{"id":40,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":39,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":42,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":41,"ship_count":5,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":5},{"id":38,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":33,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":25,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":2},{"id":37,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":35,"ship_count":2,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":2}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":5,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":4,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":36,"ship_count":6,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":1},{"id":44,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":40,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":39,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":42,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":41,"ship_count":5,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":4},{"id":38,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":43,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":25,"ship_count":1,"origin":"tetartos","destination":"duteros","owner":2,"turns_remaining":1},{"id":37,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":35,"ship_count":2,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":1}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":0,"x":-3.0,"y":5.0,"owner":null,"name":"duteros"},{"ship_count":0,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":46,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":44,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":40,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":39,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":42,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":41,"ship_count":5,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":3},{"id":38,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":43,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":45,"ship_count":3,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":37,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":4,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":46,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":44,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":40,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":39,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":42,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":41,"ship_count":5,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":2},{"id":48,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":43,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":45,"ship_count":3,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":47,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":5,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":4,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":46,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":44,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":49,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":5},{"id":47,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":42,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":41,"ship_count":5,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":1},{"id":48,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":43,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":45,"ship_count":3,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3}]} +{"planets":[{"ship_count":4,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":0,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":46,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":44,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":49,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":4},{"id":47,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":51,"ship_count":2,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":5},{"id":50,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":5},{"id":48,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":43,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":45,"ship_count":3,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":5,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":46,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":53,"ship_count":4,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":49,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":3},{"id":47,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":51,"ship_count":2,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":4},{"id":50,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":4},{"id":48,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":52,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":45,"ship_count":3,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":0,"x":3.0,"y":-5.0,"owner":null,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":54,"ship_count":4,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":5},{"id":53,"ship_count":4,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":49,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":2},{"id":47,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":51,"ship_count":2,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":3},{"id":50,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":3},{"id":48,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":52,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":5,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":54,"ship_count":4,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":4},{"id":53,"ship_count":4,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":49,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":1},{"id":56,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":51,"ship_count":2,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":2},{"id":50,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":2},{"id":55,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":52,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":5,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":54,"ship_count":4,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":3},{"id":53,"ship_count":4,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":58,"ship_count":5,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":56,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":51,"ship_count":2,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":1},{"id":50,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":1},{"id":55,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":52,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":57,"ship_count":1,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":54,"ship_count":4,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":2},{"id":53,"ship_count":4,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":58,"ship_count":5,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":56,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":59,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":57,"ship_count":1,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":4},{"id":55,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":52,"ship_count":3,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":54,"ship_count":4,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":1},{"id":61,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":10},{"id":58,"ship_count":5,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":56,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":59,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":57,"ship_count":1,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":3},{"id":55,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":60,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":4,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":0,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":63,"ship_count":5,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":5},{"id":61,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":9},{"id":58,"ship_count":5,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":56,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":59,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":57,"ship_count":1,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":2},{"id":55,"ship_count":2,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":60,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":62,"ship_count":6,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":5,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":63,"ship_count":5,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":4},{"id":61,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":8},{"id":58,"ship_count":5,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":65,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":59,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":57,"ship_count":1,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":1},{"id":64,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":60,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":62,"ship_count":6,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":4}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":5,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":63,"ship_count":5,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":3},{"id":61,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":7},{"id":67,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":65,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":59,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":66,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":64,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":60,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":62,"ship_count":6,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":3}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":4,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":63,"ship_count":5,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":2},{"id":61,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":6},{"id":67,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":65,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":69,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":66,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":64,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":60,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":62,"ship_count":6,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":2},{"id":68,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":7,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":63,"ship_count":5,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":1},{"id":61,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":67,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":65,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":69,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":66,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":64,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":71,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":62,"ship_count":6,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":1},{"id":68,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":70,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":4,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":73,"ship_count":6,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":5},{"id":61,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":67,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":65,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":69,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":66,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":64,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":71,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":72,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":68,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":70,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":73,"ship_count":6,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":4},{"id":61,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":67,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":75,"ship_count":4,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":69,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":66,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":74,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":5},{"id":71,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":72,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":68,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":70,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":5,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":4,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":73,"ship_count":6,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":3},{"id":61,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":76,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":5},{"id":75,"ship_count":4,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":69,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":70,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":74,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":4},{"id":71,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":72,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":68,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":5,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":4,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":73,"ship_count":6,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":2},{"id":61,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":76,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":4},{"id":75,"ship_count":4,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":78,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":70,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":74,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":3},{"id":71,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":72,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":77,"ship_count":5,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":5,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":73,"ship_count":6,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":1},{"id":80,"ship_count":5,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":76,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":3},{"id":75,"ship_count":4,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":78,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":79,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":5},{"id":74,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":2},{"id":77,"ship_count":5,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":4},{"id":72,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":0,"x":-3.0,"y":-5.0,"owner":null,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":81,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":80,"ship_count":5,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":76,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":2},{"id":75,"ship_count":4,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":78,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":79,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":4},{"id":74,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":1},{"id":77,"ship_count":5,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":3}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":4,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":81,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":80,"ship_count":5,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":76,"ship_count":4,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":1},{"id":82,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":78,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":79,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":3},{"id":77,"ship_count":5,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":2}]} +{"planets":[{"ship_count":4,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":81,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":80,"ship_count":5,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":84,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":82,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":78,"ship_count":2,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":79,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":2},{"id":77,"ship_count":5,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":1},{"id":83,"ship_count":2,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":81,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":80,"ship_count":5,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":84,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":82,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":86,"ship_count":4,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":10},{"id":79,"ship_count":1,"origin":"protos","destination":"duteros","owner":1,"turns_remaining":1},{"id":85,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":83,"ship_count":2,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":4}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":8,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":81,"ship_count":1,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":87,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":84,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":82,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":86,"ship_count":4,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":9},{"id":83,"ship_count":2,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":3},{"id":85,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":4,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":89,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":87,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":84,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":82,"ship_count":1,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":86,"ship_count":4,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":8},{"id":83,"ship_count":2,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":2},{"id":85,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":88,"ship_count":8,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":89,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":87,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":84,"ship_count":4,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":91,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":86,"ship_count":4,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":7},{"id":83,"ship_count":2,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":1},{"id":85,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":88,"ship_count":8,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":4},{"id":90,"ship_count":3,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":4,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":89,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":87,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":93,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":5},{"id":91,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":86,"ship_count":4,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":6},{"id":92,"ship_count":6,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":5},{"id":85,"ship_count":2,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":88,"ship_count":8,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":3},{"id":90,"ship_count":3,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4}]} +{"planets":[{"ship_count":5,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":89,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":87,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":93,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":4},{"id":91,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":86,"ship_count":4,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":92,"ship_count":6,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":4},{"id":95,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":5},{"id":88,"ship_count":8,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":2},{"id":90,"ship_count":3,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":94,"ship_count":1,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":1,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":5,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":89,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":96,"ship_count":5,"origin":"protos","destination":"helios","owner":1,"turns_remaining":5},{"id":93,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":3},{"id":91,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":86,"ship_count":4,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":92,"ship_count":6,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":3},{"id":95,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":4},{"id":88,"ship_count":8,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":1},{"id":90,"ship_count":3,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":94,"ship_count":1,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":4}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":98,"ship_count":3,"origin":"extos","destination":"helios","owner":1,"turns_remaining":5},{"id":96,"ship_count":5,"origin":"protos","destination":"helios","owner":1,"turns_remaining":4},{"id":93,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":2},{"id":91,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":86,"ship_count":4,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":92,"ship_count":6,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":2},{"id":95,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":3},{"id":97,"ship_count":5,"origin":"pemptos","destination":"helios","owner":2,"turns_remaining":5},{"id":90,"ship_count":3,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":94,"ship_count":1,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":3}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":5,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":4,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":4,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":98,"ship_count":3,"origin":"extos","destination":"helios","owner":1,"turns_remaining":4},{"id":96,"ship_count":5,"origin":"protos","destination":"helios","owner":1,"turns_remaining":3},{"id":93,"ship_count":3,"origin":"tritos","destination":"duteros","owner":2,"turns_remaining":1},{"id":100,"ship_count":2,"origin":"extos","destination":"helios","owner":1,"turns_remaining":5},{"id":86,"ship_count":4,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":92,"ship_count":6,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":1},{"id":95,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":2},{"id":97,"ship_count":5,"origin":"pemptos","destination":"helios","owner":2,"turns_remaining":4},{"id":99,"ship_count":3,"origin":"duteros","destination":"helios","owner":2,"turns_remaining":5},{"id":94,"ship_count":1,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":2}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":7,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":0,"x":3.0,"y":5.0,"owner":null,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":5,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":98,"ship_count":3,"origin":"extos","destination":"helios","owner":1,"turns_remaining":3},{"id":96,"ship_count":5,"origin":"protos","destination":"helios","owner":1,"turns_remaining":2},{"id":102,"ship_count":3,"origin":"tetartos","destination":"helios","owner":2,"turns_remaining":5},{"id":100,"ship_count":2,"origin":"extos","destination":"helios","owner":1,"turns_remaining":4},{"id":86,"ship_count":4,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":101,"ship_count":3,"origin":"protos","destination":"helios","owner":1,"turns_remaining":5},{"id":95,"ship_count":2,"origin":"tetartos","destination":"tritos","owner":2,"turns_remaining":1},{"id":97,"ship_count":5,"origin":"pemptos","destination":"helios","owner":2,"turns_remaining":3},{"id":99,"ship_count":3,"origin":"duteros","destination":"helios","owner":2,"turns_remaining":4},{"id":94,"ship_count":1,"origin":"duteros","destination":"tritos","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":null,"name":"helios"}],"expeditions":[{"id":98,"ship_count":3,"origin":"extos","destination":"helios","owner":1,"turns_remaining":2},{"id":96,"ship_count":5,"origin":"protos","destination":"helios","owner":1,"turns_remaining":1},{"id":102,"ship_count":3,"origin":"tetartos","destination":"helios","owner":2,"turns_remaining":4},{"id":100,"ship_count":2,"origin":"extos","destination":"helios","owner":1,"turns_remaining":3},{"id":104,"ship_count":7,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":5},{"id":101,"ship_count":3,"origin":"protos","destination":"helios","owner":1,"turns_remaining":4},{"id":103,"ship_count":2,"origin":"extos","destination":"protos","owner":1,"turns_remaining":5},{"id":97,"ship_count":5,"origin":"pemptos","destination":"helios","owner":2,"turns_remaining":2},{"id":99,"ship_count":3,"origin":"duteros","destination":"helios","owner":2,"turns_remaining":3}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":1,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":1,"x":0.0,"y":0.0,"owner":1,"name":"helios"}],"expeditions":[{"id":98,"ship_count":3,"origin":"extos","destination":"helios","owner":1,"turns_remaining":1},{"id":106,"ship_count":3,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":10},{"id":102,"ship_count":3,"origin":"tetartos","destination":"helios","owner":2,"turns_remaining":3},{"id":100,"ship_count":2,"origin":"extos","destination":"helios","owner":1,"turns_remaining":2},{"id":104,"ship_count":7,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":4},{"id":101,"ship_count":3,"origin":"protos","destination":"helios","owner":1,"turns_remaining":3},{"id":103,"ship_count":2,"origin":"extos","destination":"protos","owner":1,"turns_remaining":4},{"id":97,"ship_count":5,"origin":"pemptos","destination":"helios","owner":2,"turns_remaining":1},{"id":99,"ship_count":3,"origin":"duteros","destination":"helios","owner":2,"turns_remaining":2},{"id":105,"ship_count":1,"origin":"extos","destination":"protos","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":4,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":4,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":1,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":108,"ship_count":1,"origin":"helios","destination":"extos","owner":1,"turns_remaining":5},{"id":106,"ship_count":3,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":9},{"id":102,"ship_count":3,"origin":"tetartos","destination":"helios","owner":2,"turns_remaining":2},{"id":100,"ship_count":2,"origin":"extos","destination":"helios","owner":1,"turns_remaining":1},{"id":104,"ship_count":7,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":3},{"id":101,"ship_count":3,"origin":"protos","destination":"helios","owner":1,"turns_remaining":2},{"id":103,"ship_count":2,"origin":"extos","destination":"protos","owner":1,"turns_remaining":3},{"id":107,"ship_count":2,"origin":"duteros","destination":"extos","owner":2,"turns_remaining":9},{"id":99,"ship_count":3,"origin":"duteros","destination":"helios","owner":2,"turns_remaining":1},{"id":105,"ship_count":1,"origin":"extos","destination":"protos","owner":1,"turns_remaining":4}]} +{"planets":[{"ship_count":5,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":1,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":3,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":108,"ship_count":1,"origin":"helios","destination":"extos","owner":1,"turns_remaining":4},{"id":106,"ship_count":3,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":8},{"id":102,"ship_count":3,"origin":"tetartos","destination":"helios","owner":2,"turns_remaining":1},{"id":109,"ship_count":4,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":5},{"id":104,"ship_count":7,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":2},{"id":101,"ship_count":3,"origin":"protos","destination":"helios","owner":1,"turns_remaining":1},{"id":103,"ship_count":2,"origin":"extos","destination":"protos","owner":1,"turns_remaining":2},{"id":107,"ship_count":2,"origin":"duteros","destination":"extos","owner":2,"turns_remaining":8},{"id":105,"ship_count":1,"origin":"extos","destination":"protos","owner":1,"turns_remaining":3}]} +{"planets":[{"ship_count":5,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":4,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":2,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":4,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":108,"ship_count":1,"origin":"helios","destination":"extos","owner":1,"turns_remaining":3},{"id":106,"ship_count":3,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":7},{"id":111,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":109,"ship_count":4,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":4},{"id":104,"ship_count":7,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":1},{"id":110,"ship_count":4,"origin":"tritos","destination":"protos","owner":2,"turns_remaining":10},{"id":103,"ship_count":2,"origin":"extos","destination":"protos","owner":1,"turns_remaining":1},{"id":107,"ship_count":2,"origin":"duteros","destination":"extos","owner":2,"turns_remaining":7},{"id":105,"ship_count":1,"origin":"extos","destination":"protos","owner":1,"turns_remaining":2}]} +{"planets":[{"ship_count":0,"x":-6.0,"y":0.0,"owner":null,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":5,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":3,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":5,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":1,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":108,"ship_count":1,"origin":"helios","destination":"extos","owner":1,"turns_remaining":2},{"id":106,"ship_count":3,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":6},{"id":111,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":109,"ship_count":4,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":3},{"id":113,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":5},{"id":110,"ship_count":4,"origin":"tritos","destination":"protos","owner":2,"turns_remaining":9},{"id":112,"ship_count":4,"origin":"helios","destination":"extos","owner":2,"turns_remaining":5},{"id":107,"ship_count":2,"origin":"duteros","destination":"extos","owner":2,"turns_remaining":6},{"id":105,"ship_count":1,"origin":"extos","destination":"protos","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":1,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":6,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":4,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":1,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":2,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":108,"ship_count":1,"origin":"helios","destination":"extos","owner":1,"turns_remaining":1},{"id":106,"ship_count":3,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":5},{"id":111,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":109,"ship_count":4,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":2},{"id":113,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":4},{"id":110,"ship_count":4,"origin":"tritos","destination":"protos","owner":2,"turns_remaining":8},{"id":112,"ship_count":4,"origin":"helios","destination":"extos","owner":2,"turns_remaining":4},{"id":107,"ship_count":2,"origin":"duteros","destination":"extos","owner":2,"turns_remaining":5},{"id":115,"ship_count":4,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":5},{"id":114,"ship_count":5,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":2,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":2,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":5,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":2,"x":-3.0,"y":-5.0,"owner":1,"name":"extos"},{"ship_count":3,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":117,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":106,"ship_count":3,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":4},{"id":111,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":109,"ship_count":4,"origin":"pemptos","destination":"extos","owner":2,"turns_remaining":1},{"id":113,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":3},{"id":110,"ship_count":4,"origin":"tritos","destination":"protos","owner":2,"turns_remaining":7},{"id":112,"ship_count":4,"origin":"helios","destination":"extos","owner":2,"turns_remaining":3},{"id":107,"ship_count":2,"origin":"duteros","destination":"extos","owner":2,"turns_remaining":4},{"id":115,"ship_count":4,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":4},{"id":114,"ship_count":5,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":116,"ship_count":5,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":5,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":3,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":6,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":2,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":117,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":106,"ship_count":3,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":3},{"id":111,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":119,"ship_count":2,"origin":"helios","destination":"protos","owner":2,"turns_remaining":5},{"id":113,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":2},{"id":110,"ship_count":4,"origin":"tritos","destination":"protos","owner":2,"turns_remaining":6},{"id":112,"ship_count":4,"origin":"helios","destination":"extos","owner":2,"turns_remaining":2},{"id":107,"ship_count":2,"origin":"duteros","destination":"extos","owner":2,"turns_remaining":3},{"id":115,"ship_count":4,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":3},{"id":114,"ship_count":5,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":116,"ship_count":5,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":118,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":5}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":4,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":7,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":3,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":3,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":117,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":106,"ship_count":3,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":2},{"id":121,"ship_count":5,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":9},{"id":119,"ship_count":2,"origin":"helios","destination":"protos","owner":2,"turns_remaining":4},{"id":113,"ship_count":1,"origin":"protos","destination":"extos","owner":1,"turns_remaining":1},{"id":110,"ship_count":4,"origin":"tritos","destination":"protos","owner":2,"turns_remaining":5},{"id":112,"ship_count":4,"origin":"helios","destination":"extos","owner":2,"turns_remaining":1},{"id":107,"ship_count":2,"origin":"duteros","destination":"extos","owner":2,"turns_remaining":2},{"id":115,"ship_count":4,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":2},{"id":114,"ship_count":5,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":116,"ship_count":5,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":118,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":120,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":10}]} +{"planets":[{"ship_count":1,"x":-6.0,"y":0.0,"owner":1,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":1,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":5,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":8,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":7,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":4,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":117,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":106,"ship_count":3,"origin":"tetartos","destination":"extos","owner":2,"turns_remaining":1},{"id":121,"ship_count":5,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":8},{"id":119,"ship_count":2,"origin":"helios","destination":"protos","owner":2,"turns_remaining":3},{"id":123,"ship_count":1,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":10},{"id":110,"ship_count":4,"origin":"tritos","destination":"protos","owner":2,"turns_remaining":4},{"id":122,"ship_count":1,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":9},{"id":107,"ship_count":2,"origin":"duteros","destination":"extos","owner":2,"turns_remaining":1},{"id":115,"ship_count":4,"origin":"duteros","destination":"protos","owner":2,"turns_remaining":1},{"id":114,"ship_count":5,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":116,"ship_count":5,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":118,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":120,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":9}]} +{"planets":[{"ship_count":3,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":6,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":4,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":6,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":5,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":117,"ship_count":1,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1},{"id":125,"ship_count":7,"origin":"extos","destination":"helios","owner":2,"turns_remaining":5},{"id":121,"ship_count":5,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":7},{"id":119,"ship_count":2,"origin":"helios","destination":"protos","owner":2,"turns_remaining":2},{"id":123,"ship_count":1,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":9},{"id":110,"ship_count":4,"origin":"tritos","destination":"protos","owner":2,"turns_remaining":3},{"id":122,"ship_count":1,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":8},{"id":124,"ship_count":1,"origin":"protos","destination":"helios","owner":1,"turns_remaining":5},{"id":120,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":8},{"id":118,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":116,"ship_count":5,"origin":"tetartos","destination":"pemptos","owner":2,"turns_remaining":1}]} +{"planets":[{"ship_count":4,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":2,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":7,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":9,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":7,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":6,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":126,"ship_count":5,"origin":"duteros","destination":"pemptos","owner":2,"turns_remaining":11},{"id":125,"ship_count":7,"origin":"extos","destination":"helios","owner":2,"turns_remaining":4},{"id":121,"ship_count":5,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":6},{"id":119,"ship_count":2,"origin":"helios","destination":"protos","owner":2,"turns_remaining":1},{"id":123,"ship_count":1,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":8},{"id":110,"ship_count":4,"origin":"tritos","destination":"protos","owner":2,"turns_remaining":2},{"id":122,"ship_count":1,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":7},{"id":124,"ship_count":1,"origin":"protos","destination":"helios","owner":1,"turns_remaining":4},{"id":120,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":7},{"id":118,"ship_count":2,"origin":"extos","destination":"pemptos","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":7,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":3,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":8,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":8,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":8,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":5,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":126,"ship_count":5,"origin":"duteros","destination":"pemptos","owner":2,"turns_remaining":10},{"id":125,"ship_count":7,"origin":"extos","destination":"helios","owner":2,"turns_remaining":3},{"id":121,"ship_count":5,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":127,"ship_count":2,"origin":"helios","destination":"duteros","owner":2,"turns_remaining":5},{"id":123,"ship_count":1,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":7},{"id":110,"ship_count":4,"origin":"tritos","destination":"protos","owner":2,"turns_remaining":1},{"id":122,"ship_count":1,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":6},{"id":124,"ship_count":1,"origin":"protos","destination":"helios","owner":1,"turns_remaining":3},{"id":120,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":6}]} +{"planets":[{"ship_count":12,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":4,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":5,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":9,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":9,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":9,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":6,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":126,"ship_count":5,"origin":"duteros","destination":"pemptos","owner":2,"turns_remaining":9},{"id":125,"ship_count":7,"origin":"extos","destination":"helios","owner":2,"turns_remaining":2},{"id":121,"ship_count":5,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":127,"ship_count":2,"origin":"helios","destination":"duteros","owner":2,"turns_remaining":4},{"id":123,"ship_count":1,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":6},{"id":120,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":122,"ship_count":1,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":124,"ship_count":1,"origin":"protos","destination":"helios","owner":1,"turns_remaining":2}]} +{"planets":[{"ship_count":13,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":5,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":2,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":10,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":10,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":10,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":7,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":126,"ship_count":5,"origin":"duteros","destination":"pemptos","owner":2,"turns_remaining":8},{"id":125,"ship_count":7,"origin":"extos","destination":"helios","owner":2,"turns_remaining":1},{"id":121,"ship_count":5,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":3},{"id":127,"ship_count":2,"origin":"helios","destination":"duteros","owner":2,"turns_remaining":3},{"id":123,"ship_count":1,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":5},{"id":120,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":122,"ship_count":1,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":124,"ship_count":1,"origin":"protos","destination":"helios","owner":1,"turns_remaining":1},{"id":128,"ship_count":4,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":9}]} +{"planets":[{"ship_count":14,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":6,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":3,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":11,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":11,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":11,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":14,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":126,"ship_count":5,"origin":"duteros","destination":"pemptos","owner":2,"turns_remaining":7},{"id":128,"ship_count":4,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":8},{"id":121,"ship_count":5,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":127,"ship_count":2,"origin":"helios","destination":"duteros","owner":2,"turns_remaining":2},{"id":123,"ship_count":1,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":4},{"id":120,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":122,"ship_count":1,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":3}]} +{"planets":[{"ship_count":15,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":7,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":4,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":12,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":12,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":12,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":9,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":126,"ship_count":5,"origin":"duteros","destination":"pemptos","owner":2,"turns_remaining":6},{"id":128,"ship_count":4,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":7},{"id":121,"ship_count":5,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":127,"ship_count":2,"origin":"helios","destination":"duteros","owner":2,"turns_remaining":1},{"id":123,"ship_count":1,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":3},{"id":120,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":122,"ship_count":1,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":2},{"id":129,"ship_count":6,"origin":"helios","destination":"protos","owner":2,"turns_remaining":5}]} +{"planets":[{"ship_count":16,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":10,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":5,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":13,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":18,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":13,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":10,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":126,"ship_count":5,"origin":"duteros","destination":"pemptos","owner":2,"turns_remaining":5},{"id":128,"ship_count":4,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":6},{"id":129,"ship_count":6,"origin":"helios","destination":"protos","owner":2,"turns_remaining":4},{"id":122,"ship_count":1,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":1},{"id":123,"ship_count":1,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":2},{"id":120,"ship_count":3,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":17,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":11,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":6,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":14,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":17,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":14,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":8,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":126,"ship_count":5,"origin":"duteros","destination":"pemptos","owner":2,"turns_remaining":4},{"id":128,"ship_count":4,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":5},{"id":129,"ship_count":6,"origin":"helios","destination":"protos","owner":2,"turns_remaining":3},{"id":130,"ship_count":3,"origin":"helios","destination":"extos","owner":2,"turns_remaining":5},{"id":123,"ship_count":1,"origin":"protos","destination":"pemptos","owner":1,"turns_remaining":1}]} +{"planets":[{"ship_count":18,"x":-6.0,"y":0.0,"owner":2,"name":"protos"},{"ship_count":12,"x":-3.0,"y":5.0,"owner":2,"name":"duteros"},{"ship_count":7,"x":3.0,"y":5.0,"owner":2,"name":"tritos"},{"ship_count":15,"x":6.0,"y":0.0,"owner":2,"name":"tetartos"},{"ship_count":17,"x":3.0,"y":-5.0,"owner":2,"name":"pemptos"},{"ship_count":10,"x":-3.0,"y":-5.0,"owner":2,"name":"extos"},{"ship_count":9,"x":0.0,"y":0.0,"owner":2,"name":"helios"}],"expeditions":[{"id":126,"ship_count":5,"origin":"duteros","destination":"pemptos","owner":2,"turns_remaining":3},{"id":128,"ship_count":4,"origin":"tritos","destination":"pemptos","owner":2,"turns_remaining":4},{"id":129,"ship_count":6,"origin":"helios","destination":"protos","owner":2,"turns_remaining":2},{"id":130,"ship_count":3,"origin":"helios","destination":"extos","owner":2,"turns_remaining":4},{"id":131,"ship_count":5,"origin":"extos","destination":"duteros","owner":2,"turns_remaining":9}]} From 35747ae6cb501d089bb691c44d8c4a931f9e9eef Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Tue, 17 Sep 2019 14:03:16 +0200 Subject: [PATCH 8/8] fix types --- frontend/www/index.js | 36 ++++++++++++++++++------------------ frontend/www/index.ts | 5 +++++ 2 files changed, 23 insertions(+), 18 deletions(-) create mode 100644 frontend/www/index.ts diff --git a/frontend/www/index.js b/frontend/www/index.js index dcc5aa1..6967f0b 100644 --- a/frontend/www/index.js +++ b/frontend/www/index.js @@ -2,31 +2,31 @@ import { Game } from "planetwars"; import { memory } from "planetwars/plantwars_bg" import { Shader } from "./webgl/shader" +import { main } from './index.ts' + const URL = window.location.origin+window.location.pathname; const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1); const game_location = LOCATION + "static/game.json"; -// fetch(game_location) -// .then((r) => r.text()) -// .then((response) => { -// console.log(response); -// let game = Game.new(response); -// console.log(game.turn_count()); -// }).catch(console.error); +fetch(game_location) + .then((r) => r.text()) + .then((response) => { + main(Game.new(response)); + }).catch(console.error); -const g = Game.new(""); +// const g = Game.new(""); -const p1 = g.locations(); -const s1 = g.location_count(); -console.log(p1, s1); -const a1 = new Float64Array(memory.buffer, p1, s1 * 3); -console.log(a1); +// const p1 = g.locations(); +// const s1 = g.location_count(); +// console.log(p1, s1); +// const a1 = new Float64Array(memory.buffer, p1, s1 * 3); +// console.log(a1); -g.add_location(0.5, 1.2, 3.14); +// g.add_location(0.5, 1.2, 3.14); -const p2 = g.locations(); -const s2 = g.location_count(); -const a2 = new Float64Array(memory.buffer, p2, s2 * 3); -console.log(a2); +// const p2 = g.locations(); +// const s2 = g.location_count(); +// const a2 = new Float64Array(memory.buffer, p2, s2 * 3); +// console.log(a2); diff --git a/frontend/www/index.ts b/frontend/www/index.ts new file mode 100644 index 0000000..142f771 --- /dev/null +++ b/frontend/www/index.ts @@ -0,0 +1,5 @@ +import { Game } from "planetwars"; + +export function main(game: Game) { + console.log(game.turn_count()); +}