feat: reimplement priority gacha system

This commit is contained in:
Rekai Nyangadzayi Musuka 2021-03-04 22:30:36 -06:00
parent 41b6b0ea19
commit b7c673cdee
6 changed files with 152 additions and 86 deletions

View File

@ -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()`. Here's what's needed in order to call `.roll()` and `.roll10()`.
```rust ```rust
let mut file = File::open("students.json").unwrap(); let mut students_str = String::new();
let mut json = String::new(); let mut students = File::open("./examples/students.json").unwrap();
file.read_to_string(&mut json).unwrap(); students.read_to_string(&mut students_str).unwrap();
let students: Vec<Student> = serde_json::from_str(&json).unwrap(); let students: Vec<Student> = 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. 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 ```rust
// Here we construct a hypothetical banner featuring a gacha poll of
// everybody but Nozomi
let banner_students: Vec<Student> = students let banner_students: Vec<Student> = students
.iter() .iter()
.filter(|student| student.name != "ノゾミ") .filter(|student| student.name != "ノゾミ")
.map(|student| student.clone()) .map(|student| student.clone())
.collect(); .collect();
let hoshino = find_student(&students, "ホシノ").unwrap(); let hoshino = find_student(&students, "ホシノ").unwrap()
let shiroko = find_student(&students, "シロコ").unwrap(); .into_priority_student(7.0 / 2.0);
let rate_up_students = vec![shiroko, hoshino]; 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) let gacha = GachaBuilder::new(79.0, 18.5, 2.5)
.with_pool(banner_students) .with_pool(banner_students)
.with_priority(&rate_up_students, 0.7) .with_priority(&priority_students)
.finish() .finish()
.unwrap(); .unwrap();
let pickup_banner = BannerBuilder::new("ピックアップ募集") let pickup_banner = BannerBuilder::new("ピックアップ募集")
.with_name_translation(Language::English, "Rate-Up Registration") .with_name_translation(Language::English, "Rate-Up Registration")
.with_sparkable_students(&rate_up_students) .with_sparkable_students(&priority_students)
.with_gacha(&gacha) .with_gacha(&gacha)
.finish() .finish()
.unwrap(); .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 `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:
The Rest of the code consists of instantiating the Gacha and Banner structs using their respective Builders.
After this:
```rust ```rust
let student: Student = pickup_banner.roll(); let student: Student = pickup_banner.roll();
// or // or
let students: [Student; 10] = pickup_banner.roll10(); 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. to perform gacha rolls using the configurations encoded above.

View File

@ -4,34 +4,47 @@ use bluearch_recruitment::i18n::Language;
use bluearch_recruitment::student::Student; use bluearch_recruitment::student::Student;
use std::{fs::File, io::Read}; 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() { fn main() {
let mut file = File::open("./examples/students.json").unwrap(); // The Banner we're rolling from is a hypothetical banner which includes every unit in the game
let mut json = String::new(); // (e.g. including Nozomi)
file.read_to_string(&mut json).unwrap(); //
// 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<Student> = 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 students: Vec<Student> = serde_json::from_str(&students_str).unwrap();
let banner_students: Vec<Student> = students
.iter()
.filter(|student| student.name != "ノゾミ")
.map(|student| student.clone())
.collect();
// Both Hoshino and Shiroko have an increased chance of being pulled. let karin = find_student(&students, "カリン")
let hoshino = find_student(&students, "ホシノ").unwrap(); .expect("カリン is not present in ./examples/students.json")
let shiroko = find_student(&students, "シロコ").unwrap(); .into_priority_student(KARIN_RATE);
let rate_up_students = vec![shiroko, hoshino];
let gacha = GachaBuilder::new(79.0, 18.5, 2.5) let mutsuki = find_student(&students, "ムツキ")
.with_pool(banner_students) .expect("ムツキ is not present in ./examples/students.json")
.with_priority(&rate_up_students, 0.7) .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() .finish()
.unwrap(); .unwrap();
let banner = BannerBuilder::new("ピックアップ募集") // I'm some N5 loser don't judge too hard pls...
.with_name_translation(Language::English, "Rate-Up Registration") let banner = BannerBuilder::new("不運ですね。")
.with_sparkable_students(&rate_up_students) .with_name_translation(Language::English, "Unlucky, right?")
.with_sparkable_students(&sparkable)
.with_gacha(&gacha) .with_gacha(&gacha)
.finish() .finish()
.unwrap(); .unwrap();

View File

@ -1,6 +1,6 @@
use crate::gacha::{Gacha, Rarity, Recruitment}; use crate::gacha::{Gacha, Rarity, Recruitment};
use crate::i18n::{I18nString, Language}; use crate::i18n::{I18nString, Language};
use crate::student::Student; use crate::student::{PriorityStudent, Student};
use rand::distributions::{Distribution, WeightedIndex}; use rand::distributions::{Distribution, WeightedIndex};
use rand::Rng; use rand::Rng;
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
@ -101,15 +101,15 @@ impl BannerBuilder {
} }
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
enum StudentType { enum StudentType<'a> {
One = 1, // One Star One, // One Star
Two, // Two Stars Two, // Two Stars
Three, // Three Stars Three, // Three Stars
Priority, // Rate-up Student (Presumably 3*) Priority(&'a PriorityStudent),
} }
impl From<Rarity> for StudentType { impl<'a> From<Rarity> for StudentType<'a> {
fn from(rarity: Rarity) -> Self { fn from(rarity: Rarity) -> Self {
match rarity { match rarity {
Rarity::One => Self::One, Rarity::One => Self::One,
@ -119,15 +119,23 @@ impl From<Rarity> for StudentType {
} }
} }
impl TryFrom<StudentType> for Rarity { impl<'a> TryFrom<StudentType<'a>> for Rarity {
type Error = &'static str; type Error = &'static str;
fn try_from(value: StudentType) -> Result<Self, Self::Error> { fn try_from(value: StudentType) -> Result<Self, Self::Error> {
Rarity::try_from(&value)
}
}
impl<'a> TryFrom<&StudentType<'a>> for Rarity {
type Error = &'static str;
fn try_from(value: &StudentType<'a>) -> Result<Self, Self::Error> {
Ok(match value { Ok(match value {
StudentType::One => Self::One, StudentType::One => Self::One,
StudentType::Two => Self::Two, StudentType::Two => Self::Two,
StudentType::Three => Self::Three, 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 { impl Banner {
fn get_random_student(&self) -> Student { fn get_random_student(&self) -> Student {
let mut rng = rand::thread_rng(); 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] = [ let empty_vec = Vec::new();
(StudentType::One, self.gacha.get_rate(Rarity::One)), let priority_students = self.gacha.priority.as_ref().unwrap_or(&empty_vec);
(StudentType::Two, self.gacha.get_rate(Rarity::Two)), let mut rates = (
(StudentType::Three, three_star_rate), self.gacha.get_rate(Rarity::One),
(StudentType::Priority, priority_rate), 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 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)] { match &items[dist.sample(&mut rng)] {
(StudentType::Priority, _) => { (StudentType::Priority(priority_student), _) => priority_student.student().clone(),
let priority_students = &self.gacha.priority.as_ref().unwrap().0; (student_type, _) => {
let students: Vec<&Student> = student_pool
let index: usize = rng.gen_range(0..priority_students.len());
priority_students[index].clone()
}
(rarity, _) => {
let students: Vec<&Student> = students
.iter() .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(); .collect();
let index = rng.gen_range(0..students.len());
let index: usize = rng.gen_range(0..students.len());
students[index].clone() students[index].clone()
} }
} }
} }
fn get_random_student_of_rarity(&self, rarity: Rarity) -> Student { 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 students = &self.gacha.pool;
let two_star_students: Vec<&Student> = students let two_star_students: Vec<&Student> = students
.iter() .iter()
@ -200,11 +228,9 @@ impl Recruitment for Banner {
let two_star_present = students.iter().any(|student| student.rarity == Rarity::Two); let two_star_present = students.iter().any(|student| student.rarity == Rarity::Two);
if !two_star_present { if !two_star_present && students[students.len() - 1].rarity != Rarity::Three {
if students[students.len() - 1].rarity != Rarity::Three {
students[students.len() - 1] = self.get_random_student_of_rarity(Rarity::Two); students[students.len() - 1] = self.get_random_student_of_rarity(Rarity::Two);
} }
}
students students
} }

View File

@ -1,4 +1,4 @@
use crate::student::Student; use crate::student::{PriorityStudent, Student};
use serde_repr::{Deserialize_repr, Serialize_repr}; use serde_repr::{Deserialize_repr, Serialize_repr};
use std::cmp::Ordering; use std::cmp::Ordering;
#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq, Eq)]
@ -66,7 +66,7 @@ pub trait Recruitment {
pub struct GachaBuilder { pub struct GachaBuilder {
rates: Option<(usize, usize, usize)>, rates: Option<(usize, usize, usize)>,
pool: Option<Vec<Student>>, pool: Option<Vec<Student>>,
priority: Option<(Vec<Student>, usize)>, priority: Option<Vec<PriorityStudent>>,
} }
impl Default for GachaBuilder { impl Default for GachaBuilder {
@ -137,18 +137,18 @@ impl GachaBuilder {
/// # Examples /// # Examples
/// ``` /// ```
/// # use bluearch_recruitment::gacha::{GachaBuilder, Rarity}; /// # use bluearch_recruitment::gacha::{GachaBuilder, Rarity};
/// # use bluearch_recruitment::student::Student; /// # use bluearch_recruitment::student::{Student, PriorityStudent};
/// let aru = Student::new("アル", Rarity::Three); /// let aru = Student::new("アル", Rarity::Three).into_priority_student(3.5 / 2.0);
/// let hina = Student::new("ヒナ", Rarity::Three); /// let hina = Student::new("ヒナ", Rarity::Three).into_priority_student(3.5 / 2.0);
/// let rate_up = vec![aru, hina.clone()]; /// let pool = vec![aru.student().clone(), hina.student().clone()];
/// let priority = vec![hina]; /// let priority = vec![aru, hina];
/// let gacha_builder = GachaBuilder::new(79.0, 18.5, 2.5) /// let gacha_builder = GachaBuilder::new(79.0, 18.5, 2.5)
/// .with_pool(rate_up) /// .with_pool(pool)
/// .with_priority(&priority, 3.5); /// .with_priority(&priority);
/// ``` /// ```
pub fn with_priority(self, students: &[Student], total_rate: f32) -> Self { pub fn with_priority(self, students: &[PriorityStudent]) -> Self {
Self { Self {
priority: Some((students.to_vec(), (total_rate * 10.0) as usize)), priority: Some(students.to_vec()),
..self ..self
} }
} }
@ -184,7 +184,7 @@ pub struct Gacha {
/// (1★, 2★, 3★) /// (1★, 2★, 3★)
pub rates: (usize, usize, usize), pub rates: (usize, usize, usize),
pub pool: Vec<Student>, pub pool: Vec<Student>,
pub priority: Option<(Vec<Student>, usize)>, pub priority: Option<Vec<PriorityStudent>>,
} }
impl Gacha { impl Gacha {

View File

@ -60,9 +60,7 @@ impl I18nString {
/// ///
/// Will return None if there is no translation for the given language /// Will return None if there is no translation for the given language
pub fn get(&self, language: Language) -> Option<String> { pub fn get(&self, language: Language) -> Option<String> {
self.translations self.translations.get(&language).cloned()
.get(&language)
.map(|message| message.clone())
} }
} }

View File

@ -55,4 +55,32 @@ impl Student {
pub fn add_translation(&mut self, language: Language, name: &str) { pub fn add_translation(&mut self, language: Language, name: &str) {
self.name.update(language, name); 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
}
} }