aboutsummaryrefslogtreecommitdiff
path: root/planetwars-rules
diff options
context:
space:
mode:
Diffstat (limited to 'planetwars-rules')
-rw-r--r--planetwars-rules/.gitignore2
-rw-r--r--planetwars-rules/Cargo.toml11
-rw-r--r--planetwars-rules/src/config.rs85
-rw-r--r--planetwars-rules/src/lib.rs112
-rw-r--r--planetwars-rules/src/protocol.rs79
-rw-r--r--planetwars-rules/src/rules.rs192
-rw-r--r--planetwars-rules/src/serializer.rs75
7 files changed, 556 insertions, 0 deletions
diff --git a/planetwars-rules/.gitignore b/planetwars-rules/.gitignore
new file mode 100644
index 0000000..869df07
--- /dev/null
+++ b/planetwars-rules/.gitignore
@@ -0,0 +1,2 @@
+/target
+Cargo.lock \ No newline at end of file
diff --git a/planetwars-rules/Cargo.toml b/planetwars-rules/Cargo.toml
new file mode 100644
index 0000000..d42da14
--- /dev/null
+++ b/planetwars-rules/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "planetwars-rules"
+version = "0.1.0"
+authors = ["Ilion Beyst <ilion.beyst@gmail.com>"]
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+serde_json = "1.0"
+serde = { version = "1.0", features = ["derive"] }
diff --git a/planetwars-rules/src/config.rs b/planetwars-rules/src/config.rs
new file mode 100644
index 0000000..57c77eb
--- /dev/null
+++ b/planetwars-rules/src/config.rs
@@ -0,0 +1,85 @@
+use std::fs::File;
+use std::io;
+use std::io::Read;
+use std::path::PathBuf;
+
+use serde_json;
+
+use super::protocol as proto;
+use super::rules::*;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Config {
+ pub map_file: PathBuf,
+ pub max_turns: u64,
+}
+
+impl Config {
+ pub fn create_state(&self, num_players: usize) -> PwState {
+ let planets = self.load_map(num_players);
+ let players = (0..num_players)
+ .map(|player_num| Player {
+ id: player_num + 1,
+ alive: true,
+ })
+ .collect();
+
+ PwState {
+ 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<Planet> {
+ 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.
+ // ignore players that are not in the game
+ if owner_num > 0 && owner_num <= num_players {
+ Some(owner_num - 1)
+ } 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<Map> {
+ 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);
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Map {
+ pub planets: Vec<proto::Planet>,
+}
diff --git a/planetwars-rules/src/lib.rs b/planetwars-rules/src/lib.rs
new file mode 100644
index 0000000..48034ee
--- /dev/null
+++ b/planetwars-rules/src/lib.rs
@@ -0,0 +1,112 @@
+#[macro_use]
+extern crate serde;
+extern crate serde_json;
+
+pub mod config;
+pub mod protocol;
+pub mod rules;
+pub mod serializer;
+
+pub use config::Config as PwConfig;
+pub use protocol::CommandError;
+pub use rules::{Dispatch, PwState};
+use std::collections::HashMap;
+
+pub struct PlanetWars {
+ /// Game state
+ state: rules::PwState,
+ /// Map planet names to their ids
+ planet_map: HashMap<String, usize>,
+}
+
+impl PlanetWars {
+ pub fn create(config: PwConfig, num_players: usize) -> Self {
+ let state = config.create_state(num_players);
+
+ let planet_map = state
+ .planets
+ .iter()
+ .map(|p| (p.name.clone(), p.id))
+ .collect();
+
+ PlanetWars { state, planet_map }
+ }
+
+ /// Proceed to next turn
+ pub fn step(&mut self) {
+ self.state.repopulate();
+ self.state.step();
+ }
+
+ pub fn is_finished(&self) -> bool {
+ self.state.is_finished()
+ }
+
+ pub fn serialize_state(&self) -> protocol::State {
+ serializer::serialize(&self.state)
+ }
+
+ pub fn serialize_player_state(&self, player_id: usize) -> protocol::State {
+ serializer::serialize_rotated(&self.state, player_id - 1)
+ }
+
+ pub fn state<'a>(&'a self) -> &'a PwState {
+ &self.state
+ }
+
+ /// Execute a command
+ pub fn execute_command(
+ &mut self,
+ player_num: usize,
+ cmd: &protocol::Command,
+ ) -> Result<(), CommandError> {
+ let dispatch = self.parse_command(player_num, cmd)?;
+ self.state.dispatch(&dispatch);
+ return Ok(());
+ }
+
+ /// Check the given command for validity.
+ /// If it is valid, return an internal representation of the dispatch
+ /// described by the command.
+ pub fn parse_command(
+ &self,
+ player_id: usize,
+ cmd: &protocol::Command,
+ ) -> Result<Dispatch, CommandError> {
+ let origin_id = *self
+ .planet_map
+ .get(&cmd.origin)
+ .ok_or(CommandError::OriginDoesNotExist)?;
+
+ let target_id = *self
+ .planet_map
+ .get(&cmd.destination)
+ .ok_or(CommandError::DestinationDoesNotExist)?;
+
+ if self.state.planets[origin_id].owner() != Some(player_id - 1) {
+ println!("owner was {:?}", self.state.planets[origin_id].owner());
+ return Err(CommandError::OriginNotOwned);
+ }
+
+ if self.state.planets[origin_id].ship_count() < cmd.ship_count {
+ return Err(CommandError::NotEnoughShips);
+ }
+
+ if cmd.ship_count == 0 {
+ return Err(CommandError::ZeroShipMove);
+ }
+
+ Ok(Dispatch {
+ origin: origin_id,
+ target: target_id,
+ ship_count: cmd.ship_count,
+ })
+ }
+
+ /// Execute a dispatch.
+ /// This assumes the dispatch is valid. You should check this yourself
+ /// or use `parse_command` to obtain a valid dispatch.
+ pub fn execute_dispatch(&mut self, dispatch: &Dispatch) {
+ self.state.dispatch(dispatch);
+ }
+}
diff --git a/planetwars-rules/src/protocol.rs b/planetwars-rules/src/protocol.rs
new file mode 100644
index 0000000..23612d0
--- /dev/null
+++ b/planetwars-rules/src/protocol.rs
@@ -0,0 +1,79 @@
+#[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<usize>,
+ pub name: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Action {
+ #[serde(rename = "moves")]
+ pub commands: Vec<Command>,
+}
+
+#[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<Planet>,
+ pub expeditions: Vec<Expedition>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GameInfo {
+ pub players: Vec<String>,
+}
+
+#[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<CommandError>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
+#[serde(tag = "type", content = "value")]
+pub enum PlayerAction {
+ Timeout,
+ ParseError(String),
+ Commands(Vec<PlayerCommand>),
+}
+
+#[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),
+}
diff --git a/planetwars-rules/src/rules.rs b/planetwars-rules/src/rules.rs
new file mode 100644
index 0000000..587098f
--- /dev/null
+++ b/planetwars-rules/src/rules.rs
@@ -0,0 +1,192 @@
+/// The planet wars game rules.
+#[derive(Debug)]
+pub struct PwState {
+ pub players: Vec<Player>,
+ pub planets: Vec<Planet>,
+ pub expeditions: Vec<Expedition>,
+ // 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: usize,
+ pub alive: bool,
+}
+
+#[derive(Debug)]
+pub struct Fleet {
+ pub owner: Option<usize>,
+ pub ship_count: u64,
+}
+
+#[derive(Debug)]
+pub struct Planet {
+ pub id: usize,
+ pub name: String,
+ pub fleets: Vec<Fleet>,
+ 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 PwState {
+ 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<usize> {
+ self.players
+ .iter()
+ .filter_map(|p| if p.alive { Some(p.id) } else { None })
+ .collect()
+ }
+}
+
+impl Planet {
+ pub fn owner(&self) -> Option<usize> {
+ 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;
+ }
+}
diff --git a/planetwars-rules/src/serializer.rs b/planetwars-rules/src/serializer.rs
new file mode 100644
index 0000000..7eb2e01
--- /dev/null
+++ b/planetwars-rules/src/serializer.rs
@@ -0,0 +1,75 @@
+use super::protocol as proto;
+use super::rules::{Expedition, Planet, PwState};
+
+/// Serialize given gamestate
+pub fn serialize(state: &PwState) -> proto::State {
+ serialize_rotated(state, 0)
+}
+
+/// Serialize given gamestate with player numbers rotated by given offset.
+pub fn serialize_rotated(state: &PwState, offset: usize) -> proto::State {
+ let serializer = Serializer::new(state, offset);
+ serializer.serialize_state()
+}
+
+struct Serializer<'a> {
+ state: &'a PwState,
+ player_num_offset: usize,
+}
+
+impl<'a> Serializer<'a> {
+ fn new(state: &'a PwState, offset: usize) -> Self {
+ Serializer {
+ state: state,
+ player_num_offset: offset,
+ }
+ }
+
+ 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 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 + num_players - self.player_num_offset) % num_players;
+ // protocol player ids start at 1
+ return rotated_id + 1;
+ }
+
+ fn serialize_planet(&self, planet: &Planet) -> proto::Planet {
+ proto::Planet {
+ name: planet.name.clone(),
+ x: planet.x,
+ y: planet.y,
+ owner: planet.owner().map(|id| self.player_num(id)),
+ ship_count: planet.ship_count(),
+ }
+ }
+
+ fn serialize_expedition(&self, exp: &Expedition) -> proto::Expedition {
+ proto::Expedition {
+ id: exp.id,
+ owner: self.player_num(exp.fleet.owner.unwrap()),
+ ship_count: exp.fleet.ship_count,
+ 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,
+ }
+ }
+}