diff --git a/secert-hitler/Cargo.lock b/secert-hitler/Cargo.lock new file mode 100644 index 0000000..894de54 --- /dev/null +++ b/secert-hitler/Cargo.lock @@ -0,0 +1,272 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" +dependencies = [ + "memchr", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "crossbeam-channel" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061" +dependencies = [ + "crossbeam-utils", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "ppv-lite86" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" + +[[package]] +name = "secert-hitler" +version = "0.1.0" +dependencies = [ + "crossbeam-channel", + "env_logger", + "json", + "log", + "rand", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/secert-hitler/Cargo.toml b/secert-hitler/Cargo.toml new file mode 100644 index 0000000..21c5852 --- /dev/null +++ b/secert-hitler/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "secert-hitler" +version = "0.1.0" +authors = ["bel "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "*" +crossbeam-channel = "*" +json = "*" +env_logger = "*" +log = "*" diff --git a/secert-hitler/src/config.rs b/secert-hitler/src/config.rs new file mode 100644 index 0000000..a08dabc --- /dev/null +++ b/secert-hitler/src/config.rs @@ -0,0 +1,23 @@ +pub static PORT:&str = ":8080"; +pub static MIN_PLAYERS:usize = 5; +pub static MAX_PLAYERS:usize = 10; + +pub fn players_to_facists(n: usize) -> Result { + match n { + 5 => Ok(2), + 6 => Ok(2), + 7 => Ok(3), + 8 => Ok(3), + 9 => Ok(4), + 10 => Ok(4), + _ => Err("unsupported number of players".to_string()), + } +} + +pub fn players_to_policies_facist(n: usize) -> usize { + 11 - (n-5)/2 +} + +pub fn players_to_policies_liberal(_: usize) -> usize { + 6 +} diff --git a/secert-hitler/src/controller/gamemaster/gameevent.rs b/secert-hitler/src/controller/gamemaster/gameevent.rs new file mode 100644 index 0000000..41ed748 --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/gameevent.rs @@ -0,0 +1,285 @@ +use super::super::super::model::state::event::Event; +use json; + +#[derive(Clone, Debug)] +pub struct GameEvent { + pub d: json::JsonValue, + pub sender: String, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum GameEventType { + Null, + GameStart, + RoleSet, + ElectionPend, + ElectionSet, + VoteSet, + VoteFailed, + CardPend, + CardPick, + PolicySet, + SpecialInspect, + SpecialSelect, + SpecialKill, + SpecialPeek, + GameStop, +} + +impl GameEventType { + pub fn from_string(s: String) -> GameEventType { + let cases = vec![ + GameEventType::Null, + GameEventType::GameStart, + GameEventType::RoleSet, + GameEventType::ElectionPend, + GameEventType::ElectionSet, + GameEventType::VoteSet, + GameEventType::CardPend, + GameEventType::CardPick, + GameEventType::PolicySet, + GameEventType::SpecialInspect, + GameEventType::SpecialSelect, + GameEventType::SpecialKill, + GameEventType::SpecialPeek, + GameEventType::GameStop, + ]; + for c in &cases { + if format!("{:?}", c) == s { + return c.clone(); + } + } + GameEventType::Null + } + + pub fn build(&self) -> GameEvent { + let d = json::object!{ + "body": json::object!{ + GameEventType: format!("{:?}", self), + sources: [], + targets: [], + params: [], + }.dump(), + }; + GameEvent::new(Event{sender: "__gm__".to_string(), body: d.dump(), since: "".to_string()}) + } +} + +impl GameEvent { + pub fn new(event: Event) -> GameEvent { + let top_level = json::parse(&event.body.clone()); + if top_level.is_err() { + return GameEvent{d: json::Null, sender: event.sender.clone()}; + } + let top_level = top_level.unwrap(); + if !top_level["body"].is_string() { + return GameEvent{d: json::Null, sender: event.sender.clone()}; + } + let body = top_level["body"].as_str().unwrap(); + let d = json::parse(&body).unwrap_or(json::Null); + let sender = event.sender.clone(); + if d.is_object() && d.has_key("sender") && d["sender"].is_string() { + // sender = d["sender"].as_str().unwrap(); + } + GameEvent{ + d: d, + sender: sender, + } + } + + pub fn mode(&self) -> GameEventType { + if self.d.is_null() { + return GameEventType::Null; + } + if !self.d["GameEventType"].is_string() { + return GameEventType::Null; + } + let s = self.d["GameEventType"].as_str().unwrap().to_string(); + GameEventType::from_string(s.to_string()) + } + + pub fn sources(&self) -> Vec { + self.str_vec("sources") + } + + pub fn targets(&self) -> Vec { + self.str_vec("targets") + } + + pub fn params(&self) -> Vec { + self.str_vec("params") + } + + pub fn str_vec(&self, key: &str) -> Vec { + let mut out = vec![]; + if self.d[key].is_string() { + out.push(self.d[key].as_str().unwrap().to_string()); + return out; + } + if self.d[key].is_array() { + let iter = self.d[key].members(); + for i in iter { + if !i.is_string() { + return vec![]; + } + out.push(i.as_str().unwrap().to_string()); + } + return out; + } + out + } + + pub fn serialize(&self) -> String { + (json::object!{ + body: json::stringify(json::object!{ + GameEventType: format!("{:?}", self.mode()), + sources: self.sources(), + targets: self.targets(), + params: self.params(), + }), + msgtype: "m.text".to_string(), + }).dump() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use log::{debug, LevelFilter}; + + fn init() { + let _ = env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Trace) + .try_init(); + } + + #[test] + fn new() { + let ge = GameEvent::new(Event{sender: "".to_string(), body: "\"a\"".to_string(), since: "".to_string()}); + assert!(ge.d.is_null()); + + let ge = GameEvent::new(Event{sender: "".to_string(), body: "a".to_string(), since: "".to_string()}); + assert!(ge.d.is_null()); + assert!(ge.mode() == GameEventType::Null); + } + + #[test] + fn type_from_string() { + assert!(GameEventType::from_string("Null".to_string()) == GameEventType::Null); + assert!(GameEventType::from_string("GameStart".to_string()) == GameEventType::GameStart); + assert!(GameEventType::from_string("RoleSet".to_string()) == GameEventType::RoleSet); + assert!(GameEventType::from_string("ElectionPend".to_string()) == GameEventType::ElectionPend); + assert!(GameEventType::from_string("ElectionSet".to_string()) == GameEventType::ElectionSet); + assert!(GameEventType::from_string("VoteSet".to_string()) == GameEventType::VoteSet); + assert!(GameEventType::from_string("CardPend".to_string()) == GameEventType::CardPend); + assert!(GameEventType::from_string("CardPick".to_string()) == GameEventType::CardPick); + assert!(GameEventType::from_string("PolicySet".to_string()) == GameEventType::PolicySet); + assert!(GameEventType::from_string("SpecialInspect".to_string()) == GameEventType::SpecialInspect); + assert!(GameEventType::from_string("SpecialSelect".to_string()) == GameEventType::SpecialSelect); + assert!(GameEventType::from_string("SpecialKill".to_string()) == GameEventType::SpecialKill); + assert!(GameEventType::from_string("SpecialPeek".to_string()) == GameEventType::SpecialPeek); + assert!(GameEventType::from_string("GameStop".to_string()) == GameEventType::GameStop); + } + + #[test] + fn type_build() { + let cases = vec![ + GameEventType::Null, + GameEventType::GameStart, + GameEventType::RoleSet, + GameEventType::ElectionPend, + GameEventType::ElectionSet, + GameEventType::VoteSet, + GameEventType::CardPend, + GameEventType::CardPick, + GameEventType::PolicySet, + GameEventType::SpecialInspect, + GameEventType::SpecialSelect, + GameEventType::SpecialKill, + GameEventType::SpecialPeek, + GameEventType::GameStop, + ]; + for c in &cases { + let gameevent = c.build(); + assert!(*c == gameevent.mode(), "{:?}.build() yielded {:?}", c, gameevent); + } + } + + #[test] + fn sources() { + let d = json::object!{"body": json::object!{"sources": ["a", "b"]}.dump()}; + let ge = GameEvent::new(Event{sender: "".to_string(), body: d.dump(), since: "".to_string()}); + assert!(ge.sources() == vec!["a", "b"]); + assert!(ge.params().len() == 0); + assert!(ge.targets().len() == 0); + } + + #[test] + fn targets() { + let d = json::object!{"body": json::object!{"targets": ["a", "b"]}.dump()}; + let ge = GameEvent::new(Event{sender: "".to_string(), body: d.dump(), since: "".to_string()}); + assert!(ge.targets() == vec!["a", "b"]); + assert!(ge.params().len() == 0); + assert!(ge.sources().len() == 0); + } + + #[test] + fn params() { + let empty: Vec = vec![]; + + let d = json::object!{"body": json::object!{"params": ["a", "b"]}.dump()}; + let ge = GameEvent::new(Event{sender: "".to_string(), body: d.dump(), since: "".to_string()}); + assert!(ge.params() == vec!["a", "b"]); + assert!(ge.targets().len() == 0); + assert!(ge.sources().len() == 0); + + let d = json::object!{"body": json::object!{"params": []}.dump()}; + let ge = GameEvent::new(Event{sender: "".to_string(), body: d.dump(), since: "".to_string()}); + assert!(ge.params() == empty); + assert!(ge.targets().len() == 0); + assert!(ge.sources().len() == 0); + + let d = json::object!{"body": json::object!{}.dump()}; + let ge = GameEvent::new(Event{sender: "".to_string(), body: d.dump(), since: "".to_string()}); + assert!(ge.params() == empty); + assert!(ge.targets().len() == 0); + assert!(ge.sources().len() == 0); + } + + #[test] + fn serialize() { + init(); + let ge = GameEvent{ + d: json::object!{}, + sender: "a".to_string(), + }; + assert!(ge.serialize() == r#"{"body":"{\"GameEventType\":\"Null\",\"sources\":[],\"targets\":[],\"params\":[]}","msgtype":"m.text"}"#, "{}", ge.serialize()); + + let ge = GameEvent{ + d: json::object!{GameEventType: "GameStart"}, + sender: "a".to_string(), + }; + assert!(ge.serialize() == r#"{"body":"{\"GameEventType\":\"GameStart\",\"sources\":[],\"targets\":[],\"params\":[]}","msgtype":"m.text"}"#, "{}", ge.serialize()); + + let ge = GameEvent{ + d: json::object!{GameEventType: "GameStart", params: ["hi"]}, + sender: "a".to_string(), + }; + assert!(ge.serialize() == r#"{"body":"{\"GameEventType\":\"GameStart\",\"sources\":[],\"targets\":[],\"params\":[\"hi\"]}","msgtype":"m.text"}"#, "{}", ge.serialize()); + + let ge = GameEvent{ + d: json::object!{GameEventType: "GameStart", sources: ["hi"], targets: ["hi2"]}, + sender: "a".to_string(), + }; + assert!(ge.serialize() == r#"{"body":"{\"GameEventType\":\"GameStart\",\"sources\":[\"hi\"],\"targets\":[\"hi2\"],\"params\":[]}","msgtype":"m.text"}"#, "{}", ge.serialize()); + + let ge = GameEvent{ + d: json::object!{GameEventType: "Null", sources: ["hi"], targets: ["hi2"]}, + sender: "a".to_string(), + }; + assert!(ge.serialize() == r#"{"body":"{\"GameEventType\":\"Null\",\"sources\":[\"hi\"],\"targets\":[\"hi2\"],\"params\":[]}","msgtype":"m.text"}"#, "{}", ge.serialize()); + debug!("sample gameevent serialize: {}", ge.serialize()); + } +} diff --git a/secert-hitler/src/controller/gamemaster/gamemaster/game.rs b/secert-hitler/src/controller/gamemaster/gamemaster/game.rs new file mode 100644 index 0000000..f6d945b --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/gamemaster/game.rs @@ -0,0 +1,600 @@ +use super::gamemaster::GameMaster; +use super::super::gameevent::GameEvent; +use super::super::gameevent::GameEventType; +use super::super::policy::Policy; +use super::super::role::Role; + +use log::{debug, error}; +use json; +use std::collections::HashMap; + +impl GameMaster { + pub fn run_game(&mut self) -> Result { + loop { + let ge = self.game_is_over()?; + self.room.send(ge.serialize()); + let ge = self.game_election()?; + self.room.send(ge.serialize()); + let ge = self.game_is_over()?; + self.room.send(ge.serialize()); + let p = match self.game_election_vote().unwrap_or(GameEventType::GameStop.build()).mode() { + GameEventType::VoteFailed => self.game_policy_select_random(), + GameEventType::GameStop => self.game_is_over(), + _ => self.game_policy_select(), + }?; + self.room.send(p.serialize()); + if p.mode() == GameEventType::Null { + continue; + } else if p.mode() != GameEventType::PolicySet { + error!("unexpected game event type after election vote followup: {:?}", p); + return Err(Role::Null); + } + let params = p.params(); + let param = params.first(); + if param.is_none() { + error!("unexpected missing param on {:?}: {:?}", p, param); + return Err(Role::Null); + } + let param = param.unwrap(); + let policy = Policy::from_string(param.to_string()); + let ge = self.game_policy_veto(policy.clone())?; + self.room.send(ge.serialize()); + let ge = self.game_ends_with(policy.clone())?; + self.room.send(ge.serialize()); + let ge = self.game_policy_enact(policy.clone())?; + self.room.send(ge.serialize()); + } + } + + pub fn game_is_over(&mut self) -> Result { + if self.policies[&Policy::Facist] >= 3 { + let chancellor = self.chancellor.clone(); + if chancellor.is_some() { + let player = self.player(chancellor.unwrap()); + if player.is_some() && player.unwrap().get_role() == Role::Hitler { + return Err(Role::Facist); + } + } + } + if self.policies[&Policy::Facist] > 5 { + return Err(Role::Facist); + } + if self.policies[&Policy::Liberal] > 5 { + return Err(Role::Liberal); + } + Ok(GameEventType::Null.build()) + } + + pub fn game_election(&mut self) -> Result { + let mut ge = GameEventType::ElectionPend.build(); + let president_candidate = self.candidate_presidents.pop().unwrap(); + self.president = Some(president_candidate.clone()); + ge.d["targets"] = json::array![president_candidate.clone()]; + ge.d["params"] = json::array!["president"]; + self.room.send(ge.serialize()).unwrap(); + let events = self.scrape_until_gameeventtype(GameEventType::ElectionSet); + if events.is_err() { + return Err(Role::Null); + } + let events = events.unwrap(); + let chancellor_candidate_event = GameEvent::new(events.last().unwrap().clone()); + let chancellor_candidate = chancellor_candidate_event.targets(); + if chancellor_candidate.len() == 0 { + debug!("no chancellor candidates found in election set"); + return self.game_election(); + } + let chancellor_candidate = chancellor_candidate.last().unwrap(); + if self.player(chancellor_candidate.clone()).is_none() { + debug!("invalid chancellor candidates found in election set"); + return self.game_election(); + } + self.chancellor = Some(chancellor_candidate.clone()); + Ok(chancellor_candidate_event) + } + + pub fn game_election_vote(&mut self) -> Result { + let mut votes: HashMap = HashMap::new(); + while votes.len() < self.players().len() { + debug!("votes: {:?}", votes); + let events = self.scrape_until_gameeventtype(GameEventType::VoteSet).unwrap(); + debug!("scrape until vote set found {:?}", events); + if events.len() > 0 { + let ge = GameEvent::new(events.last().unwrap().clone()); + let sources = ge.sources(); + if sources.len() > 0 { + let player = sources.first().unwrap(); + if self.player(player.clone()).is_some() { + let params = ge.params(); + if params.len() > 0 { + votes.insert(player.clone(), params.first().unwrap() == "y"); + } + } + } + } + } + debug!("game election vote yielded: {:?}", votes); + + let mut ge = GameEventType::VoteSet.build(); + ge.d["sources"] = json::array!["__gm__"]; + let mut yays = 0; + for (_, vote) in &votes { + if *vote { + yays += 1; + } + } + let gm_vote = yays > self.players().len() / 2; + ge.d["params"] = json::array![format!("{:?}", gm_vote)]; + for (player, vote) in &votes { + ge.d["sources"].push(player.clone()); + ge.d["params"].push(format!("{:?}", vote)); + } + self.room.send(ge.serialize()); + + if ! gm_vote { + self.president = None; + self.chancellor = None; + return Ok(GameEventType::VoteFailed.build()); + } + + if self.game_is_over().is_err() { + return Ok(GameEventType::GameStop.build()); + } + Ok(GameEventType::Null.build()) + } + + pub fn game_policy_select_random(&mut self) -> Result { + self.failed_votes += 1; + if self.failed_votes < 3 { + return Ok(GameEventType::Null.build()); + } + let policy = self.deck.pop().unwrap().clone(); + self.discard.push(policy.clone()); + let mut ge = GameEventType::PolicySet.build(); + ge.d["params"] = json::array![format!("{:?}", policy)]; + Ok(ge) + } + + pub fn game_policy_select(&mut self) -> Result { + let mut draw = vec![]; + let mut to_discard = vec![]; + for _ in 0..3 { + draw.push(self.deck.pop().unwrap().clone()); + to_discard.push(self.deck.pop().unwrap().clone()); + } + + let mut ge = GameEventType::CardPend.build(); + if self.president.is_none() { + return Err(Role::Null); + } + ge.d["targets"] = json::array![self.president.clone().unwrap().clone()]; + ge.d["params"] = json::array![]; + for policy in &draw { + ge.d["params"].push(format!("{:?}", policy)); + } + self.room.send(ge.serialize()).unwrap(); + + draw.clear(); + loop { + let events = self.scrape_until_gameeventtype(GameEventType::CardPick); + if events.is_err() { + return Err(Role::Null); + } + let events = events.unwrap(); + if events.len() == 0 { + return Err(Role::Null); + } + let card_pick_event = GameEvent::new(events.last().unwrap().clone()); + let sources = card_pick_event.sources(); + if sources.len() == 0 || sources.first().unwrap().clone() != self.president.clone().unwrap() { + continue + } + let params = card_pick_event.params(); + if params.len() != 2 { + continue + } + for param in ¶ms { + let policy = Policy::from_string(param.clone()); + if policy != Policy::Null { + draw.push(policy); + } + } + if draw.len() != params.len() { + continue; + } + break; + } + + let mut ge = GameEventType::CardPend.build(); + if self.chancellor.is_none() { + return Err(Role::Null); + } + ge.d["targets"] = json::array![self.chancellor.clone().unwrap().clone()]; + ge.d["params"] = json::array![]; + for policy in &draw { + ge.d["params"].push(format!("{:?}", policy)); + } + self.room.send(ge.serialize()).unwrap(); + + draw.clear(); + loop { + let events = self.scrape_until_gameeventtype(GameEventType::CardPick); + if events.is_err() { + return Err(Role::Null); + } + let events = events.unwrap(); + if events.len() == 0 { + return Err(Role::Null); + } + let card_pick_event = GameEvent::new(events.last().unwrap().clone()); + let sources = card_pick_event.sources(); + if sources.len() == 0 || sources.first().unwrap().clone() != self.chancellor.clone().unwrap() { + continue + } + let params = card_pick_event.params(); + if params.len() != 1 { + continue + } + for param in ¶ms { + let policy = Policy::from_string(param.clone()); + if policy != Policy::Null { + draw.push(policy); + } + } + if draw.len() != params.len() { + continue; + } + break; + } + + let policy = draw[0].clone(); + for i in 0..to_discard.len() { + if policy == to_discard[i] { + to_discard.remove(i); + break; + } + } + if to_discard.len() == 3 { + return Err(Role::Null); + } + for i in &to_discard { + self.discard.push(i.clone()); + } + + let mut ge = GameEventType::PolicySet.build(); + ge.d["params"] = json::array!(format!("{:?}", policy)); + Ok(ge) + } + + pub fn game_policy_veto(&mut self, _p: Policy) -> Result { + Err(Role::Null) + } + + pub fn game_ends_with(&mut self, _p: Policy) -> Result { + Err(Role::Null) + } + + pub fn game_policy_enact(&mut self, _p: Policy) -> Result { + Err(Role::Null) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use super::super::super::super::super::model::state::mockrooms::MockRooms; + use super::super::super::super::super::model::state::rooms::Rooms; + use super::super::super::super::super::config; + use super::super::super::player::Player; + + use log::{debug, LevelFilter}; + use json; + + fn init() { + let _ = env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Trace) + .try_init(); + } + + fn dummy() -> GameMaster { + init(); + let mut mrs = MockRooms::new(); + let r = mrs.create("__gm__".to_string()); + let room_id = r.room_id().clone(); + let mut gm = GameMaster::new(r); + for i in 0..config::MIN_PLAYERS-2 { + assert!(mrs.join(i.to_string(), room_id.clone()).is_ok()); + } + mrs.join((config::MIN_PLAYERS-1).to_string(), room_id).unwrap().send(format!(r#"{{ + "msgtype": "m.text", + "body": "{{\"GameEventType\": \"GameStart\"}}" + }}"#)).unwrap(); + assert!(gm.run_lobby().is_ok()); + assert!(gm.run_game_setup().is_ok()); + gm + } + + #[test] + fn test_dummy() { + let gm = dummy(); + assert!(gm.players().len() == config::MIN_PLAYERS); + assert!(config::players_to_policies_facist(gm.players().len()) + config::players_to_policies_liberal(gm.players().len()) == gm.deck.len()); + assert!(gm.players().len() < gm.candidate_presidents.len()); + assert!(gm.policies[&Policy::Facist] == 0); + assert!(gm.policies[&Policy::Liberal] == 0); + let mut found = false; + for i in 1..gm.candidate_presidents.len() { + found = found || gm.candidate_presidents[i-1] > gm.candidate_presidents[i]; + } + assert!(found); + debug!("gamemaster dummy: {:?}", gm); + } + + #[test] + fn game_is_over() { + let mut gm = dummy(); + assert!(gm.game_is_over().is_ok()); + + let mut gm = dummy(); + gm.policies.insert(Policy::Facist, 10); + assert!(gm.game_is_over().is_err()); + assert!(gm.game_is_over().err().unwrap() == Role::Facist); + + let mut gm = dummy(); + gm.policies.insert(Policy::Liberal, 10); + assert!(gm.game_is_over().err().unwrap() == Role::Liberal); + + let mut gm = dummy(); + gm.policies.insert(Policy::Facist, 3); + assert!(gm.game_is_over().is_ok()); + gm.chancellor = Some("123".to_string()); + assert!(gm.game_is_over().is_ok()); + let mut p = Player::new("123".to_string()); + p.set_role(Role::Liberal); + gm.lobby.players.insert("123".to_string(), p); + assert!(gm.game_is_over().is_ok()); + + let mut gm = dummy(); + gm.policies.insert(Policy::Facist, 3); + assert!(gm.game_is_over().is_ok()); + gm.chancellor = Some("123".to_string()); + assert!(gm.game_is_over().is_ok()); + let mut p = Player::new("123".to_string()); + p.set_role(Role::Facist); + gm.lobby.players.insert("123".to_string(), p); + assert!(gm.game_is_over().is_ok()); + + let mut gm = dummy(); + gm.policies.insert(Policy::Facist, 3); + assert!(gm.game_is_over().is_ok()); + gm.chancellor = Some("123".to_string()); + assert!(gm.game_is_over().is_ok()); + let mut p = Player::new("123".to_string()); + p.set_role(Role::Hitler); + gm.lobby.players.insert("123".to_string(), p); + assert!(gm.game_is_over().is_err()); + assert!(gm.game_is_over().err().unwrap() == Role::Facist); + } + + #[test] + fn game_election() { + let mut gm = dummy(); + + debug!("sending a null event"); + let ge = GameEventType::Null.build(); + assert!(ge.mode() == GameEventType::Null); + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending an empty election set event"); + let ge = GameEventType::ElectionSet.build(); + assert!(ge.mode() == GameEventType::ElectionSet); + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending a params election set event"); + let mut ge = GameEventType::ElectionSet.build(); + assert!(ge.mode() == GameEventType::ElectionSet); + ge.d["params"] = json::array!["a"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending a sources election set event"); + let mut ge = GameEventType::ElectionSet.build(); + assert!(ge.mode() == GameEventType::ElectionSet); + ge.d["sources"] = json::array!["b"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending an empty targets election set event"); + let mut ge = GameEventType::ElectionSet.build(); + assert!(ge.mode() == GameEventType::ElectionSet); + ge.d["targets"] = json::array![]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending an invalid targets election set event"); + let mut ge = GameEventType::ElectionSet.build(); + assert!(ge.mode() == GameEventType::ElectionSet); + ge.d["targets"] = json::array!["abc"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending a valid targets election set event"); + let mut ge = GameEventType::ElectionSet.build(); + assert!(ge.mode() == GameEventType::ElectionSet); + ge.d["targets"] = json::array!["realplayer"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + gm.lobby.players.insert("realplayer".to_string(), Player::new("realplayer".to_string())); + + debug!("running game election"); + assert!(gm.game_election().is_ok()); + + let election_pends = gm.room.sync(); + assert!(election_pends.len() == 6, "election_pends: {:?}", election_pends); + } + + #[test] + fn game_election_vote() { + let mut gm = dummy(); + gm.lobby.players = HashMap::new(); + + debug!("sending a null event"); + let ge = GameEventType::Null.build(); + assert!(ge.mode() == GameEventType::Null); + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending a valid vote from a valid player"); + let mut ge = GameEventType::VoteSet.build(); + assert!(ge.mode() == GameEventType::VoteSet); + ge.d["sources"] = json::array!["1"]; + ge.d["params"] = json::array!["n"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + gm.lobby.players.insert("1".to_string(), Player::new("1".to_string())); + + debug!("sending a valid repeat vote from a valid player"); + let mut ge = GameEventType::VoteSet.build(); + assert!(ge.mode() == GameEventType::VoteSet); + ge.d["sources"] = json::array!["1"]; + ge.d["params"] = json::array!["y"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending an invalid vote from a valid player"); + let mut ge = GameEventType::VoteSet.build(); + assert!(ge.mode() == GameEventType::VoteSet); + ge.d["sources"] = json::array![]; + ge.d["params"] = json::array!["n"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + gm.lobby.players.insert("2".to_string(), Player::new("2".to_string())); + + debug!("correcting an invalid vote from a valid player"); + let mut ge = GameEventType::VoteSet.build(); + assert!(ge.mode() == GameEventType::VoteSet); + ge.d["sources"] = json::array!["2"]; + ge.d["params"] = json::array!["y"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + let ge = gm.game_election_vote(); + debug!("game election vote result: {:?}", ge); + assert!(ge.is_ok()); + + let events = gm.room.sync(); + assert!(events.len() == 1); + let e = events.last().unwrap().clone(); + let ge = GameEvent::new(e.clone()); + assert!(ge.sender == "__gm__", "post-game election vote ge: {:?} from {:?}", ge, e); + assert!(ge.mode() == GameEventType::VoteSet); + assert!(ge.sources()[0] == "__gm__"); + assert!(ge.params()[0] == "true"); + assert!(ge.params()[1] == "true"); + assert!(ge.params()[2] == "true"); + } + + #[test] + fn game_policy_select() { + let mut gm = dummy(); + gm.president = Some("president".to_string()); + gm.chancellor = Some("chancellor".to_string()); + + debug!("sending an irrelevant event to pres picking a policy"); + let mut ge = GameEventType::Null.build(); + assert!(ge.mode() == GameEventType::Null); + ge.d["sources"] = json::array!["2"]; + ge.d["params"] = json::array!["y"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending a invalid president card pick"); + let mut ge = GameEventType::CardPick.build(); + assert!(ge.mode() == GameEventType::CardPick); + ge.d["sources"] = json::array!["president"]; + ge.d["params"] = json::array!["abc"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending a short president card pick"); + let mut ge = GameEventType::CardPick.build(); + assert!(ge.mode() == GameEventType::CardPick); + ge.d["sources"] = json::array!["president"]; + ge.d["params"] = json::array!["Facist"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending a valid president card pick"); + let mut ge = GameEventType::CardPick.build(); + assert!(ge.mode() == GameEventType::CardPick); + ge.d["sources"] = json::array!["president"]; + ge.d["params"] = json::array!["Facist", "Liberal"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending an irrelevant event to chan picking a policy"); + let mut ge = GameEventType::Null.build(); + assert!(ge.mode() == GameEventType::Null); + ge.d["sources"] = json::array!["2"]; + ge.d["params"] = json::array!["y"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending a invalid chan card pick"); + let mut ge = GameEventType::CardPick.build(); + assert!(ge.mode() == GameEventType::CardPick); + ge.d["sources"] = json::array!["chancellor"]; + ge.d["params"] = json::array!["abc"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending a short chan card pick"); + let mut ge = GameEventType::CardPick.build(); + assert!(ge.mode() == GameEventType::CardPick); + ge.d["sources"] = json::array!["chancellor"]; + ge.d["params"] = json::array![]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + debug!("sending a valid chan card pick"); + let mut ge = GameEventType::CardPick.build(); + assert!(ge.mode() == GameEventType::CardPick); + ge.d["sources"] = json::array!["chancellor"]; + ge.d["params"] = json::array!["Facist"]; + assert!(gm.room.send(ge.serialize()).is_ok()); + debug!("sent: {}", ge.serialize()); + + let ge = gm.game_policy_select(); + assert!(ge.is_ok()); + let ge = ge.unwrap(); + assert!(ge.mode() == GameEventType::PolicySet); + assert!(ge.params().len() == 1); + assert!(ge.params().first().unwrap() == "Facist"); + assert!(gm.discard.len() == 2); + // todo assert cards picked were viable + } + + #[test] + fn game_policy_select_random() { + let mut gm = dummy(); + gm.failed_votes = 0; + let ge = gm.game_policy_select_random(); + assert!(ge.is_ok()); + assert!(ge.unwrap().mode() == GameEventType::Null); + assert!(gm.discard.len() == 0); + + let mut gm = dummy(); + gm.failed_votes = 2; + let ge = gm.game_policy_select_random(); + assert!(ge.is_ok()); + let ge = ge.unwrap(); + assert!(ge.clone().mode() == GameEventType::PolicySet); + assert!(ge.clone().params().len() == 1); + let policy = Policy::from_string(ge.clone().params().first().clone().unwrap().clone()); + assert!(policy != Policy::Null); + assert!(gm.discard.len() == 1); + assert!(gm.discard.first().unwrap().clone() == policy); + } +} diff --git a/secert-hitler/src/controller/gamemaster/gamemaster/gamemaster.rs b/secert-hitler/src/controller/gamemaster/gamemaster/gamemaster.rs new file mode 100644 index 0000000..ab14c8c --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/gamemaster/gamemaster.rs @@ -0,0 +1,153 @@ +use super::super::super::super::model::state::room::Room; +use super::super::super::super::model::state::event::Event; +use super::super::lobby::Lobby; +use super::super::player::Player; +use super::super::policy::Policy; +use super::super::role::Role; +use super::super::gameevent::GameEventType; +use super::super::gameevent::GameEvent; + +use log::{info, debug, error}; +use std::collections::HashMap; +use std::thread; +use std::time; + +#[derive(Debug)] +pub struct GameMaster { + pub room: Box, + pub lobby: Lobby, + pub candidate_presidents: Vec, + pub deck: Vec, + pub discard: Vec, + pub policies: HashMap, + pub president: Option, + pub chancellor: Option, + pub failed_votes: usize, +} + +impl GameMaster { + pub fn new(room: Box) -> GameMaster { + info!("created for room {}", room.room_id()); + GameMaster{ + room: room, + lobby: Lobby::new(), + candidate_presidents: vec![], + deck: vec![], + discard: vec![], + policies: HashMap::new(), + president: None, + chancellor: None, + failed_votes: 0, + } + } + + pub fn run(&mut self) -> Result { + loop { + let r = self.run_lobby(); + if r.is_ok() { + break + } + error!("error running lobby: {:?}", r); + } + self.run_game_setup()?; + self.run_game() + } + + pub fn players(&self) -> Vec { + let mut players = vec![]; + for k in self.lobby.players.keys() { + players.push(k.clone()); + } + players + } + + pub fn player(&mut self, id: String) -> Option<&mut Player> { + self.lobby.players.get_mut(&id) + } + + pub fn scrape_until_gameeventtype(&mut self, get: GameEventType) -> Result, String> { + let mut scraped = vec![]; + loop { + let events = self.room.sync(); + for e in &events { + scraped.push(e.clone()); + let ge = GameEvent::new(e.clone()); + debug!("scrape_until {:?}: {:?}: ge: {:?}", get, ge.mode() == get, ge); + if ge.mode() == get { + debug!("/scrape_until {:?}: {:?}: ge: {:?}", get, ge.mode() == get, ge); + self.room.rollback(e.since.clone()); + return Ok(scraped); + } + } + thread::sleep(time::Duration::new(1, 0)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use super::super::super::super::super::model::state::mockroom::MockRoom; + use super::super::super::super::super::model::state::mockrooms::MockRooms; + use super::super::super::super::super::model::state::rooms::Rooms; + + #[test] + fn new_mockroom() { + let _ = GameMaster::new(Box::new(MockRoom::create("__gm__".to_string()))); + } + + #[test] + fn new_rooms_mockrooms() { + fn get() -> impl Rooms { + let mrs = MockRooms::new(); + mrs + } + let mut mrs = get(); + let r = mrs.create("__gm__".to_string()); + let _ = GameMaster::new(r); + } + + #[test] + fn new_mockrooms() { + let mut mrs = MockRooms::new(); + let r = mrs.create("__gm__".to_string()); + let _ = GameMaster::new(r); + } + + #[test] + fn scrape_until_get() { + let mut mrs = MockRooms::new(); + let r = mrs.create("__gm__".to_string()); + let room_id = r.room_id().clone(); + let r2 = mrs.join("r2".to_string(), room_id); + assert!(r2.is_ok()); + let mut r2 = r2.unwrap(); + let mut gm = GameMaster::new(r); + gm.room.sync(); + r2.send(GameEventType::Null.build().serialize()).unwrap(); + let scraped = gm.scrape_until_gameeventtype(GameEventType::Null); + assert!(scraped.is_ok()); + let scraped = scraped.unwrap(); + assert!(scraped.len() == 1); + assert!(scraped.first().is_some()); + let e = scraped.first().unwrap(); + assert!(GameEvent::new(e.clone()).mode() == GameEventType::Null, "{:?}", e); + assert!(gm.room.sync().len() == 0); + + r2.send(GameEventType::ElectionSet.build().serialize()).unwrap(); + r2.send(GameEventType::ElectionSet.build().serialize()).unwrap(); + let scraped = gm.scrape_until_gameeventtype(GameEventType::ElectionSet); + assert!(scraped.is_ok()); + let scraped = scraped.unwrap(); + assert!(scraped.len() == 1); + assert!(gm.room.sync().len() == 1); + + r2.send(GameEventType::SpecialPeek.build().serialize()).unwrap(); + r2.send(GameEventType::ElectionPend.build().serialize()).unwrap(); + let scraped = gm.scrape_until_gameeventtype(GameEventType::ElectionPend); + assert!(scraped.is_ok()); + let scraped = scraped.unwrap(); + assert!(scraped.len() == 2); + assert!(gm.room.sync().len() == 0); + } +} diff --git a/secert-hitler/src/controller/gamemaster/gamemaster/lobby.rs b/secert-hitler/src/controller/gamemaster/gamemaster/lobby.rs new file mode 100644 index 0000000..4061344 --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/gamemaster/lobby.rs @@ -0,0 +1,85 @@ +use super::gamemaster::GameMaster; + +use super::super::gameevent::GameEvent; +use super::super::gameevent::GameEventType; + +use std::thread; +use std::time; + +impl GameMaster { + pub fn run_lobby(&mut self) -> Result { + loop { + let r = self.run_lobby_scrape().clone(); + if r.clone().is_err() { + return r; + } + if r.clone().unwrap_or(0) != 0 { + return r; + } + thread::sleep(time::Duration::new(1, 0)); + } + } + + pub fn run_lobby_scrape(&mut self) -> Result { + let events = self.scrape_until_gameeventtype(GameEventType::GameStart)?; + for e in &events { + self.lobby.eat(e.clone()); + } + if events.len() > 0 { + let last = events.last().unwrap(); + let ge = GameEvent::new(last.clone()); + if ge.mode() == GameEventType::GameStart { + self.lobby.lock(); + } + } + return self.lobby.ready(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use super::super::super::super::super::model::state::mockrooms::MockRooms; + use super::super::super::super::super::model::state::rooms::Rooms; + use super::super::super::super::super::config; + use log::{LevelFilter}; + + fn init() { + let _ = env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Trace) + .try_init(); + } + + #[test] + fn run_lobby() { + init(); + let mut mrs = MockRooms::new(); + let r1 = mrs.create("__gm__".to_string()); + let room_id = r1.room_id(); + let mut gm = GameMaster::new(r1); + for i in 0..config::MIN_PLAYERS-1 { + let mut r2 = mrs.join(i.to_string(), room_id.clone()).unwrap(); + r2.send(format!(r#"{{ + "msgtype": "m.text", + "body": "{{\"GameEventType\": \"GameStart\"}}" + }}"#)).unwrap(); + let ready = gm.run_lobby(); + assert!(ready.is_err() == (i != 3), "want {:?} for ready.is_err #{:?}, which is {:?}", i != 3, i, ready.is_err()); + } + assert!(gm.lobby.players.len() == 5, "first run_lobby players: {:?}, sync: {:?}", gm.lobby.players, gm.room.sync()); + let mut players1 = vec![]; + for k in gm.lobby.players.keys() { + players1.push(k.clone()); + } + let mut r2 = mrs.join("r2".to_string(), room_id.clone()).unwrap(); + r2.send(format!(r#"{{ + "msgtype": "m.text", + "body": "{{\"GameEventType\": \"GameStart\"}}" + }}"#)).unwrap(); + assert!(gm.run_lobby().is_ok()); + assert!(gm.lobby.players.len() == 5, "secnd run_lobby players: {:?}, sync: {:?}", gm.lobby.players, gm.room.sync()); + let players2 = gm.lobby.players.keys(); + assert!(format!("{:?}", players1) == format!("{:?}", players2)); + } +} diff --git a/secert-hitler/src/controller/gamemaster/gamemaster/mod.rs b/secert-hitler/src/controller/gamemaster/gamemaster/mod.rs new file mode 100644 index 0000000..fa9bfd8 --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/gamemaster/mod.rs @@ -0,0 +1,4 @@ +pub mod gamemaster; +mod lobby; +mod setup; +mod game; diff --git a/secert-hitler/src/controller/gamemaster/gamemaster/setup.rs b/secert-hitler/src/controller/gamemaster/gamemaster/setup.rs new file mode 100644 index 0000000..871056d --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/gamemaster/setup.rs @@ -0,0 +1,191 @@ +use super::gamemaster::GameMaster; + +use super::super::super::super::config; +use super::super::role::Role; +use super::super::policy::Policy; +use super::super::rand::rand_usize; +use super::super::rand::shuffle; + +use log::{debug, error, LevelFilter}; + +impl GameMaster { + pub fn run_game_setup(&mut self) -> Result { + self.setup_gather_candidates()?; + debug!("players: {:?}", self.candidate_presidents); + self.setup_set_roles()?; + debug!("/players: {:?}", self.candidate_presidents); + self.setup_order_candidates()?; + debug!("/players: {:?}", self.candidate_presidents); + self.setup_deck()?; + Ok("ok".to_string()) + } + + pub fn setup_gather_candidates(&mut self) -> Result { + for player in self.players() { + let p = self.player(player.clone()); + if p.is_none() { + error!("missing player {}", player); + return Err(Role::Null); + } + self.candidate_presidents.push(player.clone()); + debug!("player = {}", player); + } + Ok("ok".to_string()) + } + + pub fn setup_set_roles(&mut self) -> Result { + for player in self.players() { + self.player(player.clone()).unwrap().set_role(Role::Liberal); + } + let n = config::players_to_facists(self.players().len()); + if n.is_err() { + return Err(Role::Null); + } + let n = n.unwrap(); + for i in 0..n { + debug!("picking facist {}/{} for {} players", i, n, self.players().len()); + loop { + let j = rand_usize(self.players().len()); + let id = self.candidate_presidents[j].clone(); + let player = self.player(id.clone()).unwrap(); + if player.get_role() != Role::Liberal { + continue; + } + let role = match i { + 0 => Role::Hitler, + _ => Role::Facist, + }; + player.set_role(role); + break; + } + } + self.president = None; + self.chancellor = None; + Ok("ok".to_string()) + } + + pub fn setup_order_candidates(&mut self) -> Result { + shuffle(&mut self.candidate_presidents); + let n = self.candidate_presidents.len(); + for _ in 0..5 { + for i in 0..n { + self.candidate_presidents.push(self.candidate_presidents[i].clone()); + } + } + Ok("ok".to_string()) + } + + pub fn setup_deck(&mut self) -> Result { + for _ in 0..config::players_to_policies_facist(self.players().len()) { + self.deck.push(Policy::Facist); + } + + for _ in 0..config::players_to_policies_liberal(self.players().len()) { + self.deck.push(Policy::Liberal); + } + + shuffle(&mut self.deck); + + self.policies.insert(Policy::Facist, 0); // todo start iwth 1 if 5 players? + self.policies.insert(Policy::Liberal, 0); // todo start iwth 1 if 5 players? + + Ok("deck is loaded".to_string()) + } +} + +mod tests { + use super::*; + use super::super::super::super::super::model::state::mockrooms::MockRooms; + use super::super::super::super::super::model::state::rooms::Rooms; + use super::super::super::player::Player; + + fn init() { + let _ = env_logger::builder() + .is_test(true) + .filter_level(LevelFilter::Trace) + .try_init(); + } + + #[test] + fn run_game_setup() { + init(); + let mut mrs = MockRooms::new(); + let r1 = mrs.create("__gm__".to_string()); + let mut gm = GameMaster::new(r1); + let r = gm.run_game_setup(); + assert!(!r.is_ok()); + for i in 0..config::MIN_PLAYERS { + let id = format!("{}", i); + gm.lobby.players.insert(id.clone(), Player::new(id.clone())); + } + let r = gm.run_game_setup(); + assert!(r.is_ok(), "failed to start game after sufficient players joined: {:?}", r); + assert!(gm.candidate_presidents.len() > gm.lobby.players.len()); + assert!(gm.deck.len() > 1); + } + + #[test] + fn setup_gather_candidates() { + init(); + let mut mrs = MockRooms::new(); + let r1 = mrs.create("__gm__".to_string()); + let mut gm = GameMaster::new(r1); + assert!(gm.setup_gather_candidates().is_ok()); + for i in 0..config::MIN_PLAYERS { + let id = format!("{}", i); + gm.lobby.players.insert(id.clone(), Player::new(id.clone())); + } + assert!(gm.setup_gather_candidates().is_ok()); + for i in config::MIN_PLAYERS..config::MAX_PLAYERS+1 { + let id = format!("{}", i); + gm.lobby.players.insert(id.clone(), Player::new(id.clone())); + } + assert!(gm.setup_gather_candidates().is_ok()); + } + + #[test] + fn setup_set_roles() { + init(); + let mut mrs = MockRooms::new(); + let r1 = mrs.create("__gm__".to_string()); + let mut gm = GameMaster::new(r1); + assert!(gm.setup_set_roles().is_err()); + for i in 0..config::MIN_PLAYERS { + let id = format!("{}", i); + gm.lobby.players.insert(id.clone(), Player::new(id.clone())); + gm.candidate_presidents.push(id.clone()); + } + assert!(gm.setup_set_roles().is_ok()); + for i in config::MIN_PLAYERS..config::MAX_PLAYERS+1 { + let id = format!("{}", i); + gm.lobby.players.insert(id.clone(), Player::new(id.clone())); + gm.candidate_presidents.push(id.clone()); + } + assert!(gm.setup_set_roles().is_err()); + } + + #[test] + fn setup_order_candidates() { + init(); + let mut mrs = MockRooms::new(); + let r1 = mrs.create("__gm__".to_string()); + let mut gm = GameMaster::new(r1); + assert!(gm.setup_order_candidates().is_ok()); + gm.candidate_presidents = ["1".to_string()].to_vec(); + assert!(gm.setup_order_candidates().is_ok()); + assert!(gm.candidate_presidents.len() > 1); + gm.candidate_presidents = [].to_vec(); + for i in 0..50 { + gm.candidate_presidents.push(format!("{}", i)); + } + let was = format!("{:?}", gm.candidate_presidents); + assert!(gm.setup_order_candidates().is_ok()); + assert!(gm.candidate_presidents.len() > 50); + assert!(was != format!("{:?}", gm.candidate_presidents)); + let mut found = false; + for i in 1..gm.candidate_presidents.len() { + found = found || gm.candidate_presidents[i] < gm.candidate_presidents[i-1]; + } + assert!(found); + } +} diff --git a/secert-hitler/src/controller/gamemaster/lobby.rs b/secert-hitler/src/controller/gamemaster/lobby.rs new file mode 100644 index 0000000..1b028e0 --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/lobby.rs @@ -0,0 +1,95 @@ +use super::player::Player; +use super::super::super::model::state::event::Event; +use super::super::super::config; + +use std::collections::HashMap; + +#[derive(Clone, Debug)] +pub struct Lobby { + pub players: HashMap, + pub locked: bool, +} + +impl Lobby { + pub fn new() -> Lobby { + Lobby{ + players: HashMap::new(), + locked: false, + } + } + + pub fn eat(&mut self, message: Event) { + if self.locked { + return; + } + let j = message.join(); + if j.is_none() { + return; + } + let id = j.unwrap(); + self.players.insert(id.clone(), Player::new(id)); + } + + pub fn lock(&mut self) { + if self.ready().is_err() { + return; + } + self.locked = true; + } + + pub fn ready(&self) -> Result { + let n: usize = self.players.len(); + if n < config::MIN_PLAYERS { + return Err("not enough players".to_string()); + } + if n > config::MAX_PLAYERS { + return Err("too many players".to_string()); + } + Ok(n) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn _dummy_event(m: &str) -> Event { + Event{ + body: m.to_string(), + since: "a".to_string(), + sender: "b".to_string(), + } + } + + #[test] + fn new_lobby() { + let _ = Lobby::new(); + } + + #[test] + fn eat_join() { + let mut l = Lobby::new(); + let e = _dummy_event(r#"{"membership": "join", "displayname": "a"}"#); + let was = l.players.len(); + l.eat(e); + assert!(was+1 == l.players.len(), "want {}, got {}: {:?}", was+1, l.players.len(), l.players); + } + + #[test] + fn eat_join_malformatted() { + let mut l = Lobby::new(); + let e = _dummy_event(r#"{"membership": "join"}"#); + let was = l.players.len(); + l.eat(e); + assert!(was == l.players.len()); + } + + #[test] + fn eat_null() { + let mut l = Lobby::new(); + let e = _dummy_event(r#"{"hello": "world"}"#); + let was = l.players.len(); + l.eat(e); + assert!(was == l.players.len()); + } +} diff --git a/secert-hitler/src/controller/gamemaster/mod.rs b/secert-hitler/src/controller/gamemaster/mod.rs new file mode 100644 index 0000000..758a60d --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/mod.rs @@ -0,0 +1,7 @@ +pub mod gamemaster; +pub mod player; +pub mod role; +pub mod lobby; +pub mod gameevent; +pub mod policy; +pub mod rand; diff --git a/secert-hitler/src/controller/gamemaster/player.rs b/secert-hitler/src/controller/gamemaster/player.rs new file mode 100644 index 0000000..63c8f68 --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/player.rs @@ -0,0 +1,41 @@ +use super::role::Role; + +#[derive(Clone, Debug)] +pub struct Player { + id: String, + role: Role, +} + +impl Player { + pub fn new(id: String) -> Player { + Player { + id: id, + role: Role::new(), + } + } + + pub fn get_role(&self) -> Role { + self.role.clone() + } + + pub fn set_role(&mut self, role: Role) { + self.role.set(role); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_player() { + let _ = Player::new("id".to_string()); + } + + #[test] + fn set_role() { + let mut p = Player::new("id".to_string()); + p.set_role(Role::Facist); + assert!(p.role == Role::Facist); + } +} diff --git a/secert-hitler/src/controller/gamemaster/policy.rs b/secert-hitler/src/controller/gamemaster/policy.rs new file mode 100644 index 0000000..b5198b9 --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/policy.rs @@ -0,0 +1,57 @@ +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum Policy { + Null, + Facist, + Liberal, +} + +impl Policy { + pub fn from_string(s: String) -> Policy { + let cases = [ + Policy::Null, + Policy::Facist, + Policy::Liberal, + ]; + for c in &cases { + if format!("{:?}", c) == s { + return c.clone(); + } + } + Policy::Null + } + + pub fn new() -> Policy { + Policy::Null + } + + pub fn set(&mut self, policy: Policy) { + *self = policy.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_role() { + let _ = Policy::new(); + } + + #[test] + fn set() { + let mut r = Policy::new(); + r.set(Policy::Facist); + assert!(r == Policy::Facist); + + let mut r = Policy::new(); + r.set(Policy::Liberal); + assert!(r == Policy::Liberal); + } + + fn from_string() { + assert!(Policy::from_string("".to_string()) == Policy::Null); + assert!(Policy::from_string("Liberal".to_string()) == Policy::Liberal); + assert!(Policy::from_string("Facist".to_string()) == Policy::Facist); + } +} diff --git a/secert-hitler/src/controller/gamemaster/rand.rs b/secert-hitler/src/controller/gamemaster/rand.rs new file mode 100644 index 0000000..5821690 --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/rand.rs @@ -0,0 +1,47 @@ +use rand::{self, Rng}; + +pub fn shuffle(v: &mut Vec) { + for _ in 0..v.len()*2 { + let a = rand_usize(v.len()); + let b = rand_usize(v.len()); + let t: T = v[a].clone(); + v[a] = v[b].clone(); + v[b] = t.clone(); + } +} + +pub fn rand_usize(n: usize) -> usize { + rand::thread_rng().gen_range(0, n) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[test] + fn _rand_usize() { + let mut unique: HashMap = HashMap::new(); + for _ in 0..100 { + unique.insert(rand_usize(100), true); + } + assert!(unique.len() > 1); + } + + #[test] + fn _shuffle() { + let mut items: Vec = vec![]; + let n: usize = 50; + for i in 0..n { + items.push(i); + } + assert!(items.len() == n); + shuffle(&mut items); + assert!(items.len() == n); + let mut found = false; + for i in 1..items.len() { + found = found || items[i] < items[i-1]; + } + assert!(found); + } +} diff --git a/secert-hitler/src/controller/gamemaster/role.rs b/secert-hitler/src/controller/gamemaster/role.rs new file mode 100644 index 0000000..6d3a608 --- /dev/null +++ b/secert-hitler/src/controller/gamemaster/role.rs @@ -0,0 +1,109 @@ +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Role { + Null, + Facist, + Hitler, + Liberal, +} + +impl Role { + pub fn new() -> Role { + Role::Null + } + + pub fn set(&mut self, role: Role) { + *self = role.clone() + } + + pub fn is_hitler(&self) -> bool { + self == &Role::Hitler + } + + pub fn is_facist(&self) -> bool { + self == &Role::Facist || self.is_hitler() + } + + pub fn is_liberal(&self) -> bool { + self == &Role::Liberal + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_role() { + let _ = Role::new(); + } + + #[test] + fn set() { + let mut r = Role::new(); + r.set(Role::Facist); + assert!(r == Role::Facist); + } + + #[test] + fn is_hitler_liberal() { + let mut r = Role::new(); + r.set(Role::Liberal); + assert!(!r.is_hitler()); + } + + #[test] + fn is_hitler_facist() { + let mut r = Role::new(); + r.set(Role::Facist); + assert!(!r.is_hitler()); + } + + #[test] + fn is_hitler_yes() { + let mut r = Role::new(); + r.set(Role::Hitler); + assert!(r.is_hitler()); + } + + #[test] + fn is_facist_liberal() { + let mut r = Role::new(); + r.set(Role::Liberal); + assert!(!r.is_facist()); + } + + #[test] + fn is_facist_facist() { + let mut r = Role::new(); + r.set(Role::Facist); + assert!(r.is_facist()); + } + + #[test] + fn is_facist_hitler() { + let mut r = Role::new(); + r.set(Role::Hitler); + assert!(r.is_facist()); + } + + #[test] + fn is_liberal_liberal() { + let mut r = Role::new(); + r.set(Role::Liberal); + assert!(r.is_liberal()); + } + + #[test] + fn is_liberal_facist() { + let mut r = Role::new(); + r.set(Role::Facist); + assert!(!r.is_liberal()); + } + + #[test] + fn is_liberal_hitler() { + let mut r = Role::new(); + r.set(Role::Hitler); + assert!(!r.is_liberal()); + } +} diff --git a/secert-hitler/src/controller/mod.rs b/secert-hitler/src/controller/mod.rs new file mode 100644 index 0000000..2d0280a --- /dev/null +++ b/secert-hitler/src/controller/mod.rs @@ -0,0 +1 @@ +pub mod gamemaster; diff --git a/secert-hitler/src/main.rs b/secert-hitler/src/main.rs new file mode 100644 index 0000000..25cf3de --- /dev/null +++ b/secert-hitler/src/main.rs @@ -0,0 +1,23 @@ +mod config; +mod controller; +mod model; +mod view; + + +use env_logger; + +fn main() { + env_logger::init(); + println!("{}", config::PORT); + /* +use self::model::state::rooms::Rooms; + let mut rooms = get_rooms(); + let room = rooms.create(); + let mut gamemaster = controller::gamemaster::gamemaster::GameMaster::new(room); + let _ = gamemaster; +} + +fn get_rooms() -> impl Rooms { + model::state::mockrooms::MockRooms::new() + */ +} diff --git a/secert-hitler/src/model/mod.rs b/secert-hitler/src/model/mod.rs new file mode 100644 index 0000000..266c62a --- /dev/null +++ b/secert-hitler/src/model/mod.rs @@ -0,0 +1 @@ +pub mod state; diff --git a/secert-hitler/src/model/state/event.rs b/secert-hitler/src/model/state/event.rs new file mode 100644 index 0000000..883e984 --- /dev/null +++ b/secert-hitler/src/model/state/event.rs @@ -0,0 +1,140 @@ +use json; + +#[derive(Clone, Debug)] +pub struct Event { + pub sender: String, + pub body: String, + pub since: String, +} + +#[derive(PartialEq, Eq, Debug)] +pub enum EventType { + Null, + Join, + Create, + Message, +} + +impl Event { + pub fn mode(&self) -> EventType { + let d = json::parse(&self.body).unwrap(); + if d["membership"].is_string() { + return match d["membership"].as_str().unwrap().to_string().as_ref() { + "join" => EventType::Join, + _ => EventType::Null, + }; + } + if d["creator"].is_string() { + return EventType::Create; + } + if d["msgtype"].is_string() { + return match d["msgtype"].as_str().unwrap().to_string().as_ref() { + "m.text" => EventType::Message, + _ => EventType::Null, + }; + } + EventType::Null + } + + pub fn join(&self) -> Option { + let d = json::parse(&self.body).unwrap(); + match self.mode() { + EventType::Join => true, + _ => return None, + }; + let o = d["displayname"].as_str(); + if o.is_none() { + return None; + } + Some(o.unwrap().to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn _dummy() -> Event { + Event{ + sender: "sender".to_string(), + body: r#"{}"#.to_string(), + since: "since".to_string(), + } + } + + #[test] + fn event() { + let e = Event{ + sender: "sender".to_string(), + body: "body".to_string(), + since: "since".to_string(), + }; + println!("{:?}", e); + } + + #[test] + fn mode_null() { + let mut e = _dummy(); + e.body = r#"{ + }"#.to_string(); + assert!(e.mode() == EventType::Null); + } + + #[test] + fn mode_create() { + let mut e = _dummy(); + e.body = r#"{ + "creator": "abc" + }"#.to_string(); + assert!(e.mode() == EventType::Create); + } + + #[test] + fn mode_msgtype() { + let mut e = _dummy(); + e.body = r#"{ + "msgtype": "m.text" + }"#.to_string(); + assert!(e.mode() == EventType::Message); + } + + #[test] + fn mode_join() { + let mut e = _dummy(); + e.body = r#"{ + "membership": "join" + }"#.to_string(); + assert!(e.mode() == EventType::Join); + } + + #[test] + fn join_some() { + let mut e = _dummy(); + e.body = r#"{ + "displayname": "hi", + "membership": "join" + }"#.to_string(); + let j = e.join(); + assert!(j.is_some()); + } + + #[test] + fn join_some_bad() { + let mut e = _dummy(); + e.body = r#"{ + "membership": "join" + }"#.to_string(); + let j = e.join(); + assert!(j.is_none()); + } + + #[test] + fn join_none() { + let mut e = _dummy(); + e.body = r#"{ + "a": "b" + }"#.to_string(); + let j = e.join(); + assert!(j.is_none()); + } +} diff --git a/secert-hitler/src/model/state/mockroom.rs b/secert-hitler/src/model/state/mockroom.rs new file mode 100644 index 0000000..1946f9a --- /dev/null +++ b/secert-hitler/src/model/state/mockroom.rs @@ -0,0 +1,232 @@ +use super::room::Room; +use super::event::Event; + +use rand::{self, Rng}; +use rand::distributions::Alphanumeric; +use crossbeam_channel::{unbounded, Sender, Receiver}; + +#[derive(Clone, Debug)] +pub struct MockRoom { + since: String, + room_id: String, + events_s: Sender>, + events_r: Receiver>, + pub sender: String, +} + +impl MockRoom { + pub fn create(sender: String) -> MockRoom { + MockRoom::join(sender, rands()) + } + + pub fn join(sender: String, room_id: String) -> MockRoom { + let (s, r) = unbounded(); + s.send(vec![]).ok().unwrap(); + let mut mr = MockRoom { + since: "".to_string(), + room_id: room_id.clone(), + events_s: s, + events_r: r, + sender: sender, + }; + mr.send_as(mr.sender.clone(), format!(r#"{{ + "displayname": "{}", + "membership": "join" + }}"#, mr.sender.clone())).unwrap(); + mr + } + + pub fn room(&self) -> impl Room { + self.clone() + } + + pub fn send_as(&mut self, id: String, message: String) -> Result { + let since = rands(); + let e = Event{ + sender: id, + since: since.clone(), + body: message, + }; + let mut events = self.events_r.recv().ok().unwrap(); + events.push(e); + self.events_s.send(events).ok().unwrap(); + Ok(since.clone()) + } + +} + +impl Room for MockRoom { + fn sync(&mut self) -> Vec { + let mut unseen: Vec = vec![]; + let mut since = self.since.clone(); + let events = self.events_r.recv().ok().unwrap(); + for e in &events { + if e.since == self.since { + unseen.clear(); + since = self.since.clone(); + } else { + unseen.push(e.clone()); + since = e.since.clone(); + } + } + self.events_s.send(events).ok().unwrap(); + self.since = since; + return unseen; + } + + fn send(&mut self, message: String) -> Result { + self.send_as(self.sender.clone(), message) + } + + fn room_id(&self) -> String { + self.room_id.clone() + } + + fn rollback(&mut self, since: String) { + self.since = since; + } + + fn since(&self) -> String { + self.since.clone() + } +} + +impl Drop for MockRoom { + fn drop(&mut self) { + println!("MockRoom::drop not impl"); + } +} + +pub fn rands() -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .collect::() +} + +#[cfg(test)] +mod tests { + use super::Room; + use super::MockRoom; + use super::Event; + use super::rands; + + fn _dummy() -> MockRoom { + let mut r = MockRoom::create(rands()); + r.since = "1".to_string(); + let mut events = r.events_r.recv().ok().unwrap(); + for i in 0..5 { + events.push(Event{ + sender: i.to_string(), + since: i.to_string(), + body: i.to_string(), + }); + } + r.events_s.send(events).ok().unwrap(); + r + } + + #[test] + fn randstest() { + let a = rands(); + let b = rands(); + assert!(a != b, "a == {} == b == {}", a, b); + } + + #[test] + fn create() { + let mut r: MockRoom = MockRoom::create(rands()); + println!("{:?}", r.sync()); + } + + #[test] + fn join() { + let rid = "a".to_string(); + let mut r: MockRoom = MockRoom::join(rands(), rid.to_string()); + assert!(r.room_id == rid); + let events = r.sync(); + let mut found = false; + for e in &events { + let j = e.join(); + if j.is_some() { + found = true; + } + } + assert!(found); + } + + #[test] + fn since_tracking_push_two() { + let mut r = _dummy(); + r.sync(); + let mut sinces = vec![]; + for _ in 0..10 { + sinces.push(r.send("0".to_string()).ok().unwrap()); + sinces.push(r.send("0".to_string()).ok().unwrap()); + r.sync(); + assert!(r.since == *sinces.last().unwrap()); + } + } + + #[test] + fn since_tracking_push_one() { + let mut r = _dummy(); + r.sync(); + let mut sinces = vec![]; + for _ in 0..10 { + sinces.push(r.send("0".to_string()).ok().unwrap()); + r.sync(); + assert!(r.since == *sinces.last().unwrap()); + } + } + + #[test] + fn since_tracking_push_none() { + let mut r = _dummy(); + r.sync(); + let mut sinces = vec![]; + sinces.push(r.send("0".to_string()).ok().unwrap()); + assert!(r.sync().len() == 1); + assert!(r.since == *sinces.last().unwrap(), "after one send: want {:?}, got {:?}: {:?}", *sinces.last().unwrap(), r.since, sinces); + assert!(r.sync().len() == 0); + assert!(r.since == *sinces.last().unwrap(), "after no send: want {:?}, got {:?}: {:?}", *sinces.last().unwrap(), r.since, sinces); + } + + #[test] + fn sync() { + let mut r = _dummy(); + let events = r.sync(); + assert!(events.len() == 3, "want {}, got {}: {:?}", 3, events.len(), events); + assert!(events[0].sender == "2"); + assert!(events[0].body == "2"); + assert!(events[0].since == "2"); + assert!(r.since == "4", "want since==4, got {}", r.since); + } + + #[test] + fn send() { + let mut r = _dummy(); + let message = "message".to_string(); + r.sync(); + assert!(r.send(message.clone()).ok().unwrap().len() > 0); + assert!(r.since == "4"); + let events = r.sync(); + assert!(events.len() == 1); + assert!(events[0].body == message, "want {}, got {}: {:?}", message, events[0].body, events); + assert!(r.since != "4"); + } + + #[test] + fn rollback() { + let mut r = _dummy(); + let was = r.since.to_string(); + let events = r.sync(); + assert!(events.len() == 3); + assert!(r.since == "4"); + r.rollback(was.to_string()); + assert!(r.since == was); + let events = r.sync(); + assert!(events.len() == 3); + assert!(r.since == "4"); + } +} diff --git a/secert-hitler/src/model/state/mockrooms.rs b/secert-hitler/src/model/state/mockrooms.rs new file mode 100644 index 0000000..968b4bd --- /dev/null +++ b/secert-hitler/src/model/state/mockrooms.rs @@ -0,0 +1,148 @@ +use super::rooms::Rooms; +use super::room::Room; +use super::mockroom::MockRoom; +use super::mockroom::rands; + +// #[derive(Clone, Debug)] +pub struct MockRooms { + rooms: Vec, +} + +impl MockRooms { + pub fn new() -> MockRooms { + MockRooms { + rooms: vec![], + } + } +} + +impl Rooms for MockRooms { + fn create(&mut self, sender: String) -> Box { + let room = MockRoom::create(sender); + let _room = room.room(); + self.rooms.push(room); + Box::new(_room) + } + + fn join(&self, sender: String, room_id: String) -> Result, &str> { + for r in &self.rooms { + if r.room_id() == room_id { + let mut r = r.clone(); + r.sender = sender; + let mut r = r.room(); + r.send(format!(r#"{{ + "displayname": "{}", + "membership": "join" + }}"#, rands())).unwrap(); + return Ok(Box::new(r)); + } + } + Err("not found") + } +} + +#[cfg(test)] +mod tests { + use super::MockRooms; + use super::MockRoom; + use super::Rooms; + + fn _dummy() -> MockRooms { + let mut mrs = MockRooms::new(); + for i in 0..5 { + let random = MockRoom::create(i.to_string()); + mrs.rooms.push(random); + let joined = MockRoom::join(i.to_string(), i.to_string()); + mrs.rooms.push(joined); + } + assert!(mrs.rooms.len() == 10); + mrs + } + + #[test] + fn mockrooms() { + let mrs = MockRooms::new(); + assert!(mrs.rooms.len() == 0); + } + + #[test] + fn create() { + let mut mrs = _dummy(); + let was = mrs.rooms.len(); + let _ = mrs.create("abc".to_string()); + let is = mrs.rooms.len(); + assert!(was+1 == is, "was {} rooms, want {} rooms, got {} rooms", was, was+1, is); + } + + #[test] + fn join_404() { + let mrs = _dummy(); + let was = mrs.rooms.len(); + let r = mrs.join("?".to_string(), "does not exist".to_string()); + let is = mrs.rooms.len(); + assert!(was == is, "was {} rooms, want {} rooms, got {} rooms", was, was+1, is); + assert!(!r.is_ok()); + } + + #[test] + fn join_found() { + let mrs = _dummy(); + let was = mrs.rooms.len(); + let r = mrs.join("?".to_string(), "0".to_string()); + let is = mrs.rooms.len(); + assert!(was == is, "was {} rooms, want {} rooms, got {} rooms", was, was+1, is); + assert!(r.is_ok()); + assert!(r.ok().unwrap().room_id() == "0"); + } + + #[test] + fn join_clobber() { + let mrs = _dummy(); + let mut a = mrs.join("?".to_string(), "0".to_string()).ok().unwrap(); + let mut b = mrs.join("?".to_string(), "0".to_string()).ok().unwrap(); + + assert!(a.room_id() == b.room_id()); + + let ea = a.sync(); + let eb = b.sync(); + println!("a1: {:?}, {:?}, {:?}", a.since(), a.room_id(), ea); + println!("b1: {:?}, {:?}, {:?}", b.since(), a.room_id(), eb); + assert!(ea.len() == eb.len()); + + let ea = a.sync(); + let eb = b.sync(); + println!("a2: {:?}, {:?}, {:?}", a.since(), a.room_id(), ea); + println!("b2: {:?}, {:?}, {:?}", b.since(), a.room_id(), eb); + assert!(ea.len() == eb.len()); + assert!(ea.len() == 0); + assert!(eb.len() == 0); + + assert!(a.send("from a".to_string()).is_ok()); + let ea = a.sync(); + let eb = b.sync(); + println!("a3: {:?}, {:?}, {:?}", a.since(), a.room_id(), ea); + println!("b3: {:?}, {:?}, {:?}", b.since(), a.room_id(), eb); + assert!(ea.len() == 1, "a sent a message and a received {}: {:?}", ea.len(), ea); + assert!(eb.len() == 1, "a sent a message and b received {}: {:?}", ea.len(), eb); + + assert!(b.send("from b".to_string()).is_ok()); + assert!(b.send("from b".to_string()).is_ok()); + assert!(a.sync().len() == 2); + assert!(b.sync().len() == 2); + assert!(a.send("from a".to_string()).is_ok()); + assert!(b.send("from b".to_string()).is_ok()); + assert!(a.send("from a".to_string()).is_ok()); + assert!(a.sync().len() == 3); + assert!(b.sync().len() == 3); + assert!(a.send("from a".to_string()).is_ok()); + assert!(b.send("from b".to_string()).is_ok()); + let a = a.sync(); + let b = b.sync(); + assert!(a.len() == 2); + assert!(b.len() == 2); + assert!(a[0].body == "from a"); + assert!(b[0].body == "from a"); + assert!(a[1].body == "from b"); + assert!(b[1].body == "from b"); + } +} diff --git a/secert-hitler/src/model/state/mod.rs b/secert-hitler/src/model/state/mod.rs new file mode 100644 index 0000000..ed4f153 --- /dev/null +++ b/secert-hitler/src/model/state/mod.rs @@ -0,0 +1,5 @@ +pub mod event; +pub mod room; +pub mod rooms; +pub mod mockroom; +pub mod mockrooms; diff --git a/secert-hitler/src/model/state/room.rs b/secert-hitler/src/model/state/room.rs new file mode 100644 index 0000000..8babf46 --- /dev/null +++ b/secert-hitler/src/model/state/room.rs @@ -0,0 +1,26 @@ +use super::event; + +use std::fmt; + +pub trait Room: fmt::Debug { + fn rollback(&mut self, since: String); + fn sync(&mut self) -> Vec; + fn send(&mut self, message: String) -> Result; + fn room_id(&self) -> String; + fn since(&self) -> String; +} + +#[cfg(test)] +mod tests { + use super::Room; + use super::super::mockroom::MockRoom; + + #[test] + fn mockroom() { + fn gen() -> impl Room { + let r = MockRoom::create("123".to_string()); + r + } + gen(); + } +} diff --git a/secert-hitler/src/model/state/rooms.rs b/secert-hitler/src/model/state/rooms.rs new file mode 100644 index 0000000..e73be2a --- /dev/null +++ b/secert-hitler/src/model/state/rooms.rs @@ -0,0 +1,24 @@ +use super::room::Room; + +pub trait Rooms { + fn create(&mut self, sender: String) -> Box; + fn join(&self, sender: String, room_id: String) -> Result, &str>; +} + +#[cfg(test)] +mod tests { + use super::Rooms; + use super::Room; + use super::super::mockrooms::MockRooms; + + #[test] + fn mockrooms() { + fn gen() -> impl Rooms { + let r = MockRooms::new(); + r + } + let mut rooms = gen(); + let mut room_ptr: Box = rooms.create("abc".to_string()); + assert!(room_ptr.send("hi".to_string()).is_ok()); + } +} diff --git a/secert-hitler/src/view/mod.rs b/secert-hitler/src/view/mod.rs new file mode 100644 index 0000000..e69de29