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()`.
```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<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.
```rust
// Here we construct a hypothetical banner featuring a gacha poll of
// everybody but Nozomi
let banner_students: Vec<Student> = 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.
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 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<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 banner_students: Vec<Student> = students
.iter()
.filter(|student| student.name != "ノゾミ")
.map(|student| student.clone())
.collect();
let students: Vec<Student> = 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();

View File

@ -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
#[derive(Debug, Clone)]
enum StudentType<'a> {
One, // One Star
Two, // Two 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 {
match rarity {
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;
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 {
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,11 +228,9 @@ 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 {
if !two_star_present && students[students.len() - 1].rarity != Rarity::Three {
students[students.len() - 1] = self.get_random_student_of_rarity(Rarity::Two);
}
}
students
}

View File

@ -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<Vec<Student>>,
priority: Option<(Vec<Student>, usize)>,
priority: Option<Vec<PriorityStudent>>,
}
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<Student>,
pub priority: Option<(Vec<Student>, usize)>,
pub priority: Option<Vec<PriorityStudent>>,
}
impl Gacha {

View File

@ -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<String> {
self.translations
.get(&language)
.map(|message| message.clone())
self.translations.get(&language).cloned()
}
}

View File

@ -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
}
}