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, + } + } +}