From 0e9e36c0c8ba0b454f4382986d50d490d1e5c98b Mon Sep 17 00:00:00 2001 From: ajuvercr Date: Sat, 14 Sep 2019 18:15:19 +0200 Subject: [PATCH] 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