diff --git a/README.md b/README.md index f953f40..03f4f80 100644 --- a/README.md +++ b/README.md @@ -8,51 +8,52 @@ Here's an implementation of Blue Archive's Gacha System. I believe it to be corr Here's what's needed in order to call `.roll()` and `.roll10()`. ```rust -let mut file = File::open("students.json").unwrap(); -let mut json = String::new(); -file.read_to_string(&mut json).unwrap(); +let mut students_str = String::new(); +let mut students = File::open("./examples/students.json").unwrap(); +students.read_to_string(&mut students_str).unwrap(); -let students: Vec = serde_json::from_str(&json).unwrap(); +let students: Vec = serde_json::from_str(&students_str).unwrap(); ``` This Repo contains `students.json` (in the examples directory) which is an Array where each object within the array contains a Student's Japanese name, English TL name and rarity. ```rust +// Here we construct a hypothetical banner featuring a gacha poll of +// everybody but Nozomi let banner_students: Vec = students .iter() .filter(|student| student.name != "ノゾミ") .map(|student| student.clone()) .collect(); -let hoshino = find_student(&students, "ホシノ").unwrap(); -let shiroko = find_student(&students, "シロコ").unwrap(); -let rate_up_students = vec![shiroko, hoshino]; +let hoshino = find_student(&students, "ホシノ").unwrap() + .into_priority_student(7.0 / 2.0); +let shiroko = find_student(&students, "シロコ").unwrap() + .into_priority_student(7.0 / 2.0); + +let priority_students = vec![shiroko, hoshino]; let gacha = GachaBuilder::new(79.0, 18.5, 2.5) .with_pool(banner_students) - .with_priority(&rate_up_students, 0.7) + .with_priority(&priority_students) .finish() .unwrap(); let pickup_banner = BannerBuilder::new("ピックアップ募集") .with_name_translation(Language::English, "Rate-Up Registration") - .with_sparkable_students(&rate_up_students) + .with_sparkable_students(&priority_students) .with_gacha(&gacha) .finish() .unwrap(); ``` -This example creates the ピックアップ募集 Banner which ran from 2021-02-04 to 2021-02-11. The only student in the game who was **not** in This banner was ノゾミ, so you can see her being filtered out of the list of *all** students to create a list of students in the ピックアップ募集. +After selecting all the students for the banner, we want to declare which units are on rate-up if there are any. In this example, Hoshino and Shiroko have unique rates. -After this, we want to determine which units are on rate-up if there are any. In this example, ホシノ and シロコ have increased pull rates. - -The Rest of the code consists of instantiating the Gacha and Banner structs using their respective Builders. - -After this: +The `BannerBuilder` and `GachaBuilder` structs are builders that set up the actual gacha system. After a `Banner` has successfully been built using the `Banner Builder`, we can call: ```rust let student: Student = pickup_banner.roll(); // or let students: [Student; 10] = pickup_banner.roll10(); ``` -can be called (when the `Recruitment` trait is in scope) to allow for accurate simulation of Blue Archive's Gacha. \ No newline at end of file +to perform gacha rolls using the configurations encoded above. \ No newline at end of file diff --git a/examples/ten_pull.rs b/examples/ten_pull.rs index 12fcfa7..0ee9bf0 100644 --- a/examples/ten_pull.rs +++ b/examples/ten_pull.rs @@ -4,34 +4,47 @@ use bluearch_recruitment::i18n::Language; use bluearch_recruitment::student::Student; use std::{fs::File, io::Read}; +const THREE_STAR_RATE: f32 = 2.5; +const TWO_STAR_RATE: f32 = 18.5; +const ONE_STAR_RATE: f32 = 79.0; + +const KARIN_RATE: f32 = 0.7; +const MUTSUKI_RATE: f32 = 0.3; + fn main() { - let mut file = File::open("./examples/students.json").unwrap(); - let mut json = String::new(); - file.read_to_string(&mut json).unwrap(); + // The Banner we're rolling from is a hypothetical banner which includes every unit in the game + // (e.g. including Nozomi) + // + // Karin (3*) and Mutsuki (2*) will have increased rates because I like them the most. + // Karin will have a pull-rate of 0.7%, and Mutsuki will have a pull-rate of 3.0% - let students: Vec = serde_json::from_str(&json).unwrap(); + let mut students_str = String::new(); + let mut students = File::open("./examples/students.json").unwrap(); + students.read_to_string(&mut students_str).unwrap(); - // This particular banner consists of everyone BUT Nozomi. - let banner_students: Vec = students - .iter() - .filter(|student| student.name != "ノゾミ") - .map(|student| student.clone()) - .collect(); + let students: Vec = serde_json::from_str(&students_str).unwrap(); - // Both Hoshino and Shiroko have an increased chance of being pulled. - let hoshino = find_student(&students, "ホシノ").unwrap(); - let shiroko = find_student(&students, "シロコ").unwrap(); - let rate_up_students = vec![shiroko, hoshino]; + let karin = find_student(&students, "カリン") + .expect("カリン is not present in ./examples/students.json") + .into_priority_student(KARIN_RATE); - let gacha = GachaBuilder::new(79.0, 18.5, 2.5) - .with_pool(banner_students) - .with_priority(&rate_up_students, 0.7) + let mutsuki = find_student(&students, "ムツキ") + .expect("ムツキ is not present in ./examples/students.json") + .into_priority_student(MUTSUKI_RATE); + + let sparkable = vec![karin.student().clone()]; + let priority = vec![karin, mutsuki]; + + let gacha = GachaBuilder::new(ONE_STAR_RATE, TWO_STAR_RATE, THREE_STAR_RATE) + .with_pool(students) + .with_priority(&priority) .finish() .unwrap(); - let banner = BannerBuilder::new("ピックアップ募集") - .with_name_translation(Language::English, "Rate-Up Registration") - .with_sparkable_students(&rate_up_students) + // I'm some N5 loser don't judge too hard pls... + let banner = BannerBuilder::new("不運ですね。") + .with_name_translation(Language::English, "Unlucky, right?") + .with_sparkable_students(&sparkable) .with_gacha(&gacha) .finish() .unwrap(); diff --git a/src/banner.rs b/src/banner.rs index 3a02415..e4b1e14 100644 --- a/src/banner.rs +++ b/src/banner.rs @@ -1,6 +1,6 @@ use crate::gacha::{Gacha, Rarity, Recruitment}; use crate::i18n::{I18nString, Language}; -use crate::student::Student; +use crate::student::{PriorityStudent, Student}; use rand::distributions::{Distribution, WeightedIndex}; use rand::Rng; use std::convert::{TryFrom, TryInto}; @@ -101,15 +101,15 @@ impl BannerBuilder { } } -#[derive(Debug, Clone, Copy)] -enum StudentType { - One = 1, // One Star - Two, // Two Stars - Three, // Three Stars - Priority, // Rate-up Student (Presumably 3*) +#[derive(Debug, Clone)] +enum StudentType<'a> { + One, // One Star + Two, // Two Stars + Three, // Three Stars + Priority(&'a PriorityStudent), } -impl From for StudentType { +impl<'a> From for StudentType<'a> { fn from(rarity: Rarity) -> Self { match rarity { Rarity::One => Self::One, @@ -119,15 +119,23 @@ impl From for StudentType { } } -impl TryFrom for Rarity { +impl<'a> TryFrom> for Rarity { type Error = &'static str; fn try_from(value: StudentType) -> Result { + Rarity::try_from(&value) + } +} + +impl<'a> TryFrom<&StudentType<'a>> for Rarity { + type Error = &'static str; + + fn try_from(value: &StudentType<'a>) -> Result { Ok(match value { StudentType::One => Self::One, StudentType::Two => Self::Two, StudentType::Three => Self::Three, - StudentType::Priority => return Err("Can not convert from Priority to Rarity"), + StudentType::Priority(_) => return Err("Can not convert from Priority to Rarity"), }) } } @@ -141,39 +149,59 @@ pub struct Banner { impl Banner { fn get_random_student(&self) -> Student { let mut rng = rand::thread_rng(); - let priority_rate = self.gacha.priority.as_ref().map_or(0, |tuple| tuple.1); - let three_star_rate = self.gacha.get_rate(Rarity::Three) - priority_rate; - let items: [(StudentType, usize); 4] = [ - (StudentType::One, self.gacha.get_rate(Rarity::One)), - (StudentType::Two, self.gacha.get_rate(Rarity::Two)), - (StudentType::Three, three_star_rate), - (StudentType::Priority, priority_rate), - ]; + let empty_vec = Vec::new(); + let priority_students = self.gacha.priority.as_ref().unwrap_or(&empty_vec); + let mut rates = ( + self.gacha.get_rate(Rarity::One), + self.gacha.get_rate(Rarity::Two), + self.gacha.get_rate(Rarity::Three), + ); + + for priority_student in priority_students { + match priority_student.student().rarity { + Rarity::One => rates.0 -= priority_student.rate, + Rarity::Two => rates.1 -= priority_student.rate, + Rarity::Three => rates.2 -= priority_student.rate, + }; + } + + let mut items: Vec<(StudentType, usize)> = Vec::with_capacity(3 + priority_students.len()); + items.push((StudentType::One, rates.0)); + items.push((StudentType::Two, rates.1)); + items.push((StudentType::Three, rates.2)); + + items.extend( + priority_students + .iter() + .map(|student| (StudentType::Priority(student), student.rate)), + ); let dist = WeightedIndex::new(items.iter().map(|item| item.1)).unwrap(); - let students = &self.gacha.pool; + let student_pool = &self.gacha.pool; - match items[dist.sample(&mut rng)] { - (StudentType::Priority, _) => { - let priority_students = &self.gacha.priority.as_ref().unwrap().0; - - let index: usize = rng.gen_range(0..priority_students.len()); - priority_students[index].clone() - } - (rarity, _) => { - let students: Vec<&Student> = students + match &items[dist.sample(&mut rng)] { + (StudentType::Priority(priority_student), _) => priority_student.student().clone(), + (student_type, _) => { + let students: Vec<&Student> = student_pool .iter() - .filter(|student| student.rarity == rarity.try_into().unwrap()) + .filter(|student| student.rarity == student_type.try_into().unwrap()) + .filter(|student| { + // Remove any Rate-Up Units + // TODO: Determine whether this is the right way of implementing priority gacha + !priority_students + .iter() + .any(|priority_student| student.name == priority_student.student().name) + }) .collect(); - - let index: usize = rng.gen_range(0..students.len()); + let index = rng.gen_range(0..students.len()); students[index].clone() } } } fn get_random_student_of_rarity(&self, rarity: Rarity) -> Student { + // NOTE: This does not actually follow the rules of any given banner. Only get_random_student() does. let students = &self.gacha.pool; let two_star_students: Vec<&Student> = students .iter() @@ -200,10 +228,8 @@ impl Recruitment for Banner { let two_star_present = students.iter().any(|student| student.rarity == Rarity::Two); - if !two_star_present { - if students[students.len() - 1].rarity != Rarity::Three { - students[students.len() - 1] = self.get_random_student_of_rarity(Rarity::Two); - } + if !two_star_present && students[students.len() - 1].rarity != Rarity::Three { + students[students.len() - 1] = self.get_random_student_of_rarity(Rarity::Two); } students diff --git a/src/gacha.rs b/src/gacha.rs index b84d236..1a88dbf 100644 --- a/src/gacha.rs +++ b/src/gacha.rs @@ -1,4 +1,4 @@ -use crate::student::Student; +use crate::student::{PriorityStudent, Student}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::cmp::Ordering; #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)] @@ -66,7 +66,7 @@ pub trait Recruitment { pub struct GachaBuilder { rates: Option<(usize, usize, usize)>, pool: Option>, - priority: Option<(Vec, usize)>, + priority: Option>, } impl Default for GachaBuilder { @@ -137,18 +137,18 @@ impl GachaBuilder { /// # Examples /// ``` /// # use bluearch_recruitment::gacha::{GachaBuilder, Rarity}; - /// # use bluearch_recruitment::student::Student; - /// let aru = Student::new("アル", Rarity::Three); - /// let hina = Student::new("ヒナ", Rarity::Three); - /// let rate_up = vec![aru, hina.clone()]; - /// let priority = vec![hina]; + /// # use bluearch_recruitment::student::{Student, PriorityStudent}; + /// let aru = Student::new("アル", Rarity::Three).into_priority_student(3.5 / 2.0); + /// let hina = Student::new("ヒナ", Rarity::Three).into_priority_student(3.5 / 2.0); + /// let pool = vec![aru.student().clone(), hina.student().clone()]; + /// let priority = vec![aru, hina]; /// let gacha_builder = GachaBuilder::new(79.0, 18.5, 2.5) - /// .with_pool(rate_up) - /// .with_priority(&priority, 3.5); + /// .with_pool(pool) + /// .with_priority(&priority); /// ``` - pub fn with_priority(self, students: &[Student], total_rate: f32) -> Self { + pub fn with_priority(self, students: &[PriorityStudent]) -> Self { Self { - priority: Some((students.to_vec(), (total_rate * 10.0) as usize)), + priority: Some(students.to_vec()), ..self } } @@ -184,7 +184,7 @@ pub struct Gacha { /// (1★, 2★, 3★) pub rates: (usize, usize, usize), pub pool: Vec, - pub priority: Option<(Vec, usize)>, + pub priority: Option>, } impl Gacha { diff --git a/src/i18n.rs b/src/i18n.rs index ea0ba2f..7a5a7d1 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -60,9 +60,7 @@ impl I18nString { /// /// Will return None if there is no translation for the given language pub fn get(&self, language: Language) -> Option { - self.translations - .get(&language) - .map(|message| message.clone()) + self.translations.get(&language).cloned() } } diff --git a/src/student.rs b/src/student.rs index e1b6339..107f289 100644 --- a/src/student.rs +++ b/src/student.rs @@ -55,4 +55,32 @@ impl Student { pub fn add_translation(&mut self, language: Language, name: &str) { self.name.update(language, name); } + + pub fn into_priority_student(self, rate: f32) -> PriorityStudent { + PriorityStudent { + inner: self, + rate: (rate * 10.0) as usize, + } + } +} + +/// A Priority Student is a student who has a pull-rate that is unique from +/// the rest of the rest of their peers in their star rating +#[derive(Debug, Clone)] +pub struct PriorityStudent { + inner: Student, + pub rate: usize, +} + +impl PriorityStudent { + pub fn new(student: Student, rate: f32) -> Self { + Self { + inner: student, + rate: (rate * 10.0) as usize, + } + } + + pub fn student(&self) -> &Student { + &self.inner + } }