diff --git a/Cargo.toml b/Cargo.toml index c1c9915..0961e96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +reqwest = { version = "0.11", features = ["json", "blocking"] } +serde_derive = "^1.0" +serde = "^1.0" +derive_more = "0.99.11" diff --git a/src/coins.rs b/src/coins.rs index 567496a..0a3463e 100644 --- a/src/coins.rs +++ b/src/coins.rs @@ -1,3 +1,6 @@ +extern crate derive_more; +use derive_more::Add; + /// Represents a monetary amount. /// /// The inner value is the total number of copper coins. @@ -6,7 +9,7 @@ /// one silver coin and every 100 silver coins is one gold coin. /// /// The Display trait uses this in game format. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Add)] pub struct Coins(i32); impl Coins { @@ -16,8 +19,8 @@ impl Coins { } /// Creates an amount of currency from a number of gold coins. - pub fn from_gold(gold: impl Into) -> Self { - Coins(gold.into() * 1_00_00) + pub const fn from_gold(gold: i32) -> Self { + Coins(gold * 1_00_00) } /// The number of silver coins. @@ -26,8 +29,8 @@ impl Coins { } /// Creates an amount of currency from a number of silver coins. - pub fn from_silver(silver: impl Into) -> Self { - Coins(silver.into() * 1_00) + pub const fn from_silver(silver: i32) -> Self { + Coins(silver * 1_00) } /// The number of copper coins. @@ -36,8 +39,8 @@ impl Coins { } /// Creates an amount of currency from a number of copper coins. - pub fn from_copper(copper: impl Into) -> Self { - Coins(copper.into()) + pub const fn from_copper(copper: i32) -> Self { + Coins(copper) } } diff --git a/src/dungeon.rs b/src/dungeon.rs new file mode 100644 index 0000000..26307f5 --- /dev/null +++ b/src/dungeon.rs @@ -0,0 +1,205 @@ +mod info; +use info::{DungeonInfo, PathInfo, Rewards, DUNGEONS}; + +mod user; +pub use user::UserProgress; + +use crate::Coins; + +/// Information about a dungeon. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Dungeon { + info: &'static DungeonInfo, +} + +/// Information about a path within a dungeon. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Path { + info: &'static PathInfo, + dungeon: &'static DungeonInfo, +} + +impl Dungeon { + /// Gets every dungeon. + pub fn all() -> Vec { + DUNGEONS.iter().map(|x| Self { info: x }).collect() + } + + /// Gets the name of the dungeon. + pub fn name(&self) -> &str { + self.info.long_name + } + + /// Gets a short name for this dungeon. + pub fn short_name(&self) -> &str { + self.info.short_name + } + + /// Gets the achievement id of the skin collection achievement for this dungeon. + pub fn collection_id(&self) -> u32 { + self.info.collection_id + } + + /// Gets the wallet currency id of the dungeon token for this dungeon. + pub fn currency_id(&self) -> u32 { + self.info.currency_id + } + + /// Gets all paths in this dungeon. + pub fn paths(&self) -> Vec { + self.info + .paths + .iter() + .map(|path| Path { + info: path, + dungeon: self.info, + }) + .collect() + } +} + +impl Path { + /// Gets every dungeon path. + pub fn all() -> Vec { + Dungeon::all().iter().flat_map(Dungeon::paths).collect() + } + + /// Looks up a path by its id. + pub fn from_id(path_id: &str) -> Option { + for path in Self::all() { + if path_id == path.id() { + return Some(path); + } + } + None + } + + /// Looks up a path by its index into the dungeon frequenter bits list. + pub fn from_dungeon_frequenter_index(index: u8) -> Option { + for path in Self::all() { + if Some(index) == path.dungeon_frequenter_index() { + return Some(path); + } + } + None + } + + /// Gets the dungeon that contains this path. + pub fn dungeon(&self) -> Dungeon { + Dungeon { info: self.dungeon } + } + + /// Gets the unique dungeon path id used by the GW2 API for this path. + pub fn id(&self) -> &str { + self.info.id + } + + /// Gets the name of this path. + pub fn name(&self) -> &str { + self.info.long_name + } + + /// Gets a short name for this path. This is typically used in LFG. + pub fn short_name(&self) -> &str { + self.info.short_name + } + + /// Gets the index into the dungeon frequenter achievement bits array + /// in the GW2 achievement API for this dungeon path. + pub fn dungeon_frequenter_index(&self) -> Option { + self.info.dungeon_frequenter_index + } + + /// The number of coins gotten by doing this dungeon path the first + /// time in a day. + pub fn coins(&self) -> Coins { + match self.info.rewards { + Rewards::Story { coins } => coins, + Rewards::Explorable { bonus_coins } => Coins::from_silver(26) + bonus_coins, + } + } + + /// The number of coins gotten by doing this dungeon repeatedly in a day. + pub fn repeat_coins(&self) -> Coins { + match self.info.rewards { + Rewards::Story { coins } => coins, + Rewards::Explorable { .. } => Coins::from_silver(26), + } + } + + /// The number of tokens gotten by doing this dungeon path the first + /// time in a day. + pub fn tokens(&self) -> u32 { + match self.info.rewards { + Rewards::Story { .. } => 0, + Rewards::Explorable { .. } => 100, + } + } + + /// The number of tokens gotten by doing this dungeon repeatedly in a day. + pub fn repeat_tokens(&self) -> u32 { + match self.info.rewards { + Rewards::Story { .. } => 0, + Rewards::Explorable { .. } => 20, + } + } +} + +#[cfg(test)] +/// NOTE: Many tests have hard coded constants that must be changed if dungeon rewards +/// are reworked or more dungeons are added. +mod test { + use super::*; + + #[test] + fn all_dungeons() { + assert_eq!(Dungeon::all().len(), 8); + } + + #[test] + fn all_paths() { + assert_eq!(Path::all().len(), 33); + } + + #[test] + fn from_id() { + assert_eq!(Path::from_id("coe_story").unwrap().id(), "coe_story"); + assert!(Path::from_id("bad_id").is_none()); + } + + #[test] + fn from_dungeon_frequenter_index() { + assert_eq!( + Path::from_dungeon_frequenter_index(5) + .unwrap() + .dungeon_frequenter_index(), + Some(5) + ); + assert!(Path::from_dungeon_frequenter_index(100).is_none()); + } + + #[test] + fn ac_story_rewards() { + let path = Path::from_id("ac_story").unwrap(); + + assert_eq!(Coins::from_silver(13), path.coins()); + assert_eq!(Coins::from_silver(13), path.repeat_coins()); + + assert_eq!(0, path.tokens()); + assert_eq!(0, path.repeat_tokens()); + } + + #[test] + fn ac_p1_rewards() { + let path = Path::from_id("hodgins").unwrap(); + + assert_eq!( + Coins::from_silver(50) + Coins::from_silver(26), + path.coins() + ); + assert_eq!(Coins::from_silver(26), path.repeat_coins()); + + assert_eq!(100, path.tokens()); + assert_eq!(20, path.repeat_tokens()); + } +} diff --git a/src/dungeon/info.rs b/src/dungeon/info.rs new file mode 100644 index 0000000..3a4abc2 --- /dev/null +++ b/src/dungeon/info.rs @@ -0,0 +1,444 @@ +//! Contains information about dungeons that are not available from the GW2 API. +//! This information would have to be updated if dungeon rewards are reworked +//! or if more dungeons are added. +//! +//! TODO: The amount of gold rewards are known for some story paths. +//! The missing story paths are currently using 0. + +use crate::Coins; + +/// Information about a dungeon. +#[derive(Debug, PartialEq, Eq)] +pub struct DungeonInfo { + /// The id used by the /v2/dungeons endpoint. + pub id: &'static str, + /// A user friendly short name. + pub short_name: &'static str, + /// A user friendly long name. + pub long_name: &'static str, + /// id of the achievement for collection all skins. + pub collection_id: u32, + /// id of the token used by the /v2/account/currencies endpoint. + pub currency_id: u32, + /// information about every path in this dungeon. + pub paths: &'static [PathInfo], +} + +/// Information about a path within a dungeon. +#[derive(Debug, PartialEq, Eq)] +pub struct PathInfo { + /// The id used by the /v2/account/dungeons endpoint to indicate + /// if a user has done this path today. + pub id: &'static str, + /// A user friendly short name. + pub short_name: &'static str, + /// A longer user friendly name. + pub long_name: &'static str, + /// The index inserted into the bits array for the dungeon + /// frequenter achievement when this path is done. + /// Is optional because some dungeons do not give dungeon + /// frequenter progress (currently only Arah story does not). + pub dungeon_frequenter_index: Option, + /// The rewards for doing this path + pub rewards: Rewards, +} + +/// Describes the rewards for a path. +#[derive(Debug, PartialEq, Eq)] +pub enum Rewards { + /// Story mission. No token rewards. Fixed coin reward. + Story { coins: Coins }, + /// Explorable path. 100 tokens on first per day, 20 on repeat. + /// bonus_coins + 26s on first per day. 26s on repeat. + Explorable { bonus_coins: Coins }, +} + +static AC_PATHS: [PathInfo; 4] = [ + PathInfo { + id: "ac_story", + short_name: "story", + long_name: "Story", + dungeon_frequenter_index: Some(4), + rewards: Rewards::Story { + coins: Coins::from_silver(13), + }, + }, + PathInfo { + id: "hodgins", + short_name: "p1", + long_name: "Hodgins (p1)", + dungeon_frequenter_index: Some(5), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(50), + }, + }, + PathInfo { + id: "detha", + short_name: "p2", + long_name: "Detha (p2)", + dungeon_frequenter_index: Some(6), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(50), + }, + }, + PathInfo { + id: "tzark", + short_name: "p3", + long_name: "Tzark (p3)", + dungeon_frequenter_index: Some(7), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(50), + }, + }, +]; + +static CM_PATHS: [PathInfo; 4] = [ + PathInfo { + id: "cm_story", + short_name: "story", + long_name: "Story", + dungeon_frequenter_index: Some(12), + rewards: Rewards::Story { + coins: Coins::from_silver(0), + }, + }, + PathInfo { + id: "asura", + short_name: "p1", + long_name: "Asura (p1)", + dungeon_frequenter_index: Some(13), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "seraph", + short_name: "p2", + long_name: "Seraph (p2)", + dungeon_frequenter_index: Some(14), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "butler", + short_name: "p3", + long_name: "Butler (p3)", + dungeon_frequenter_index: Some(15), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, +]; + +static TA_PATHS: [PathInfo; 4] = [ + PathInfo { + id: "ta_story", + short_name: "story", + long_name: "Story", + dungeon_frequenter_index: Some(20), + rewards: Rewards::Story { + coins: Coins::from_silver(0), + }, + }, + PathInfo { + id: "leurent", + short_name: "up", + long_name: "Leurent (Up)", + dungeon_frequenter_index: Some(21), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "vevina", + short_name: "forward", + long_name: "Vevina (Forward)", + dungeon_frequenter_index: Some(22), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "aetherpath", + short_name: "aetherpath", + long_name: "Aetherpath", + dungeon_frequenter_index: Some(23), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(66), + }, + }, +]; + +static SE_PATHS: [PathInfo; 4] = [ + PathInfo { + id: "se_story", + short_name: "story", + long_name: "Story", + dungeon_frequenter_index: Some(16), + rewards: Rewards::Story { + coins: Coins::from_silver(0), + }, + }, + PathInfo { + id: "fergg", + short_name: "p1", + long_name: "Fergg (p1)", + dungeon_frequenter_index: Some(17), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + // NOTE: The id is misspelled in the GW2 API. + // The spelling difference between the id and long_name is intentional. + id: "rasalov", + short_name: "p2", + long_name: "Rasolov (p2)", + dungeon_frequenter_index: Some(18), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "koptev", + short_name: "p3", + long_name: "Koptev (p3)", + dungeon_frequenter_index: Some(19), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, +]; + +static COF_PATHS: [PathInfo; 4] = [ + PathInfo { + id: "cof_story", + short_name: "story", + long_name: "Story", + dungeon_frequenter_index: Some(28), + rewards: Rewards::Story { + coins: Coins::from_silver(0), + }, + }, + PathInfo { + id: "ferrah", + short_name: "p1", + long_name: "Ferrah (p1)", + dungeon_frequenter_index: Some(29), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "magg", + short_name: "p2", + long_name: "Magg (p2)", + dungeon_frequenter_index: Some(30), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "rhiannon", + short_name: "p3", + long_name: "Rhiannon (p3)", + dungeon_frequenter_index: Some(31), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, +]; + +static HOTW_PATHS: [PathInfo; 4] = [ + PathInfo { + id: "hotw_story", + short_name: "story", + long_name: "Story", + dungeon_frequenter_index: Some(24), + rewards: Rewards::Story { + coins: Coins::from_silver(0), + }, + }, + PathInfo { + id: "butcher", + short_name: "p1", + long_name: "Butcher (p1)", + dungeon_frequenter_index: Some(25), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "plunderer", + short_name: "p2", + long_name: "Plunderer (p2)", + dungeon_frequenter_index: Some(26), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "zealot", + short_name: "p3", + long_name: "Zealot (p3)", + dungeon_frequenter_index: Some(27), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, +]; + +static COE_PATHS: [PathInfo; 4] = [ + PathInfo { + id: "coe_story", + short_name: "story", + long_name: "Story", + dungeon_frequenter_index: Some(0), + rewards: Rewards::Story { + coins: Coins::from_silver(0), + }, + }, + PathInfo { + id: "submarine", + short_name: "p1", + long_name: "Submarine (p1)", + dungeon_frequenter_index: Some(1), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "teleporter", + short_name: "p2", + long_name: "Teleporter (p2)", + dungeon_frequenter_index: Some(2), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, + PathInfo { + id: "front_door", + short_name: "p3", + long_name: "Front Door (p3)", + dungeon_frequenter_index: Some(3), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(35), + }, + }, +]; + +static ARAH_PATHS: [PathInfo; 5] = [ + PathInfo { + id: "arah_story", + short_name: "story", + long_name: "Story", + dungeon_frequenter_index: None, + rewards: Rewards::Story { + coins: Coins::from_silver(0), + }, + }, + PathInfo { + id: "jotun", + short_name: "p1", + long_name: "Jotun (p1)", + dungeon_frequenter_index: Some(8), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(180), + }, + }, + PathInfo { + id: "mursaat", + short_name: "p2", + long_name: "Mursaat (p2)", + dungeon_frequenter_index: Some(9), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(105), + }, + }, + PathInfo { + id: "forgotten", + short_name: "p3", + long_name: "Forgotten (p3)", + dungeon_frequenter_index: Some(10), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(50), + }, + }, + PathInfo { + id: "seer", + short_name: "p4", + long_name: "Seer (p4)", + dungeon_frequenter_index: Some(11), + rewards: Rewards::Explorable { + bonus_coins: Coins::from_silver(180), + }, + }, +]; + +pub static DUNGEONS: [DungeonInfo; 8] = [ + DungeonInfo { + id: "ascalonian_catacombs", + short_name: "ac", + long_name: "Ascalonian Catacombs", + collection_id: 1725, + currency_id: 5, + paths: &AC_PATHS, + }, + DungeonInfo { + id: "caudecus_manor", + currency_id: 9, + short_name: "cm", + long_name: "Caudecus's Manor", + collection_id: 1723, + paths: &CM_PATHS, + }, + DungeonInfo { + id: "twilight_arbor", + short_name: "ta", + long_name: "Twilight Arbor", + collection_id: 1721, + currency_id: 11, + paths: &TA_PATHS, + }, + DungeonInfo { + id: "sorrows_embrace", + short_name: "se", + long_name: "Sorrow's Embrace", + collection_id: 1722, + currency_id: 10, + paths: &SE_PATHS, + }, + DungeonInfo { + id: "citadel_of_flame", + short_name: "cof", + long_name: "Citadel of Flame", + collection_id: 1714, + currency_id: 13, + paths: &COF_PATHS, + }, + DungeonInfo { + id: "honor_of_the_waves", + short_name: "hotw", + long_name: "Honor of the Waves", + collection_id: 1718, + currency_id: 12, + paths: &HOTW_PATHS, + }, + DungeonInfo { + id: "crucible_of_eternity", + short_name: "coe", + long_name: "Crucible of Eternity", + collection_id: 1719, + currency_id: 14, + paths: &COE_PATHS, + }, + DungeonInfo { + id: "ruined_city_of_arah", + short_name: "arah", + long_name: "The Ruined City of Arah", + collection_id: 1724, + currency_id: 6, + paths: &ARAH_PATHS, + }, +]; diff --git a/src/dungeon/user.rs b/src/dungeon/user.rs new file mode 100644 index 0000000..e49b305 --- /dev/null +++ b/src/dungeon/user.rs @@ -0,0 +1,185 @@ +use super::Path; +use std::collections::HashSet; + +extern crate serde_derive; +use serde_derive::Deserialize; + +/// Tracks what dungeons a user has ran since the last daily reset +/// and from the last Dungeon Frequenter achievement completion. +#[derive(Debug, Clone)] +pub struct UserProgress { + dungeon_frequenter_progress: HashSet, + dungeons_ran_today: HashSet, +} + +#[derive(Deserialize)] +struct AchievementProgress { + id: u32, + bits: Option>, +} + +/// The achievement id of the Dungeon Frequenter achievement. +const DUNGEON_FREQUENTER_ID: u32 = 2963; + +impl UserProgress { + /// Fetches a user's dungeon progress from the GW2 API. + /// The API key requires the progression permission. + pub fn from_api_key(key: &str) -> reqwest::Result { + let mut new = Self { + dungeon_frequenter_progress: Default::default(), + dungeons_ran_today: Default::default(), + }; + + // fetch the user's achievement progress to update the Dungeon Frequenter progress + // note: gets {"text": "requires scope progression"} on permission error with 403 error. + // could improve this error message in this case, currently gets a deserialization error. + let achievement_progress: Vec = reqwest::blocking::Client::new() + .get("https://api.guildwars2.com/v2/account/achievements") + .header("Authorization", "Bearer ".to_owned() + key) + .send()? + .json()?; + for progress in achievement_progress { + if progress.id == DUNGEON_FREQUENTER_ID { + if let Some(bits) = progress.bits { + new.dungeon_frequenter_progress.extend(bits.into_iter()); + } + } + } + + // fetch the dungeons that have been ran today + let dungeons_ran_today: Vec = reqwest::blocking::Client::new() + .get("https://api.guildwars2.com/v2/account/dungeons") + .header("Authorization", "Bearer ".to_owned() + key) + .send()? + .json()?; + new.dungeons_ran_today + .extend(dungeons_ran_today.into_iter()); + + Ok(new) + } + + /// Modifies a player info as if the player ran a dungeon path. + pub fn run_path(&mut self, path: &Path) { + // handle dungeon frequenter achievement update + if let Some(index) = path.dungeon_frequenter_index() { + self.dungeon_frequenter_progress.insert(index); + if self.dungeon_frequenter_progress.len() == 8 { + self.dungeon_frequenter_progress.clear(); + } + } + + // handle daily update + self.dungeons_ran_today.insert(path.id().to_owned()); + } + + /// Modifies a player info as if daily reset happened. + pub fn daily_reset(&mut self) { + self.dungeons_ran_today.clear(); + } + + /// Gets if a dungeon path has been ran today. + pub fn has_ran_today(&self, path: &Path) -> bool { + self.dungeons_ran_today.contains(path.id()) + } + + /// Gets if a dungeon path gives dungeon frequenter credit if it was + /// ran next. + pub fn gives_dungeon_frequenter_credit(&self, path: &Path) -> bool { + match path.dungeon_frequenter_index() { + Some(index) => !self.dungeon_frequenter_progress.contains(&index), + None => false, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + /// Create a new player info with no paths ever ran for testing. + fn empty() -> UserProgress { + UserProgress { + dungeon_frequenter_progress: Default::default(), + dungeons_ran_today: Default::default(), + } + } + + #[test] + fn ran_none() { + let info = empty(); + for path in Path::all() { + // all paths that can give progress do + assert_eq!( + path.dungeon_frequenter_index().is_some(), + info.gives_dungeon_frequenter_credit(&path), + "path: {}", + path.id() + ); + // not ran any paths today + assert!(!info.has_ran_today(&path), "path: {}", path.id()); + } + } + + #[test] + fn ran_one() { + for path in Path::all() { + let mut info = empty(); + info.run_path(&path); + + assert!( + !info.gives_dungeon_frequenter_credit(&path), + "path: {}", + path.id() + ); + assert!(info.has_ran_today(&path), "path: {}", path.id()); + if path.dungeon_frequenter_index().is_some() { + assert_eq!(1, info.dungeon_frequenter_progress.len()); + } else { + assert_eq!(0, info.dungeon_frequenter_progress.len()); + } + } + } + + #[test] + fn ran_one_yesterday() { + let mut info = empty(); + let ran_path = Path::from_id("ac_story").unwrap(); + info.run_path(&ran_path); + info.daily_reset(); + + assert!(!info.has_ran_today(&ran_path)); + assert!(!info.gives_dungeon_frequenter_credit(&ran_path)); + } + + #[test] + fn frequenter_finished() { + let mut info = empty(); + [ + "ac_story", "hodgins", "detha", "tzark", "cm_story", "asura", "seraph", "butler", + "ta_story", + ] + .iter() + .map(|id| Path::from_id(id).unwrap()) + .for_each(|path| info.run_path(&path)); + + // dungeon frequenter should have completed right before ta_story, + // so only ta_story should be in the dungeon frequenter set. + assert_eq!(1, info.dungeon_frequenter_progress.len()); + assert!(info.dungeon_frequenter_progress.contains( + &Path::from_id("ta_story") + .unwrap() + .dungeon_frequenter_index() + .unwrap() + )); + } + + #[test] + fn daily_reset() { + let mut info = empty(); + let path = Path::from_id("ac_story").unwrap(); + info.run_path(&path); + info.daily_reset(); + + assert!(!info.has_ran_today(&path)); + } +} diff --git a/src/lib.rs b/src/lib.rs index 89302c8..b49c4f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,3 @@ mod coins; pub use coins::Coins; +pub mod dungeon;